예제 #1
0
 def setUp(self):
     self.pubsub = PubSub()
     SetUpTestInjections(pubsub=self.pubsub)
     self.power_data = []  # type: list
     SetUpTestInjections(power_db=':memory:')
     self.serial = RS485(SerialMock(self.power_data))
     self.store = PowerStore()
     SetUpTestInjections(power_serial=self.serial,
                         power_store=self.store)
     self.communicator = PowerCommunicator()
예제 #2
0
def setup_minimal_power_platform():
    # type: () -> None
    config = ConfigParser()
    config.read(constants.get_config_file())
    power_serial_port = config.get('OpenMotics', 'power_serial')
    if power_serial_port:
        Injectable.value(power_db=constants.get_power_database_file())
        Injectable.value(power_store=PowerStore())
        Injectable.value(power_serial=RS485(
            Serial(power_serial_port, 115200, timeout=None)))
        Injectable.value(power_communicator=PowerCommunicator())
        Injectable.value(power_controller=PowerController())
        Injectable.value(p1_controller=P1Controller())
    else:
        Injectable.value(power_store=None)
        Injectable.value(power_communicator=None)
        Injectable.value(power_controller=None)
        Injectable.value(p1_controller=None)
        Injectable.value(power_serial=None)
예제 #3
0
def setup_target_platform(target_platform, message_client_name):
    # type: (str, Optional[str]) -> None
    config = ConfigParser()
    config.read(constants.get_config_file())

    config_lock = Lock()
    metrics_lock = Lock()

    config_database_file = constants.get_config_database_file()

    # Debugging options
    try:
        debug_logger = config.get('OpenMotics', 'debug_logger')
        if debug_logger:
            logging.getLogger(debug_logger).setLevel(logging.DEBUG)
    except NoOptionError:
        pass

    # Webserver / Presentation layer
    try:
        https_port = int(config.get('OpenMotics', 'https_port'))
    except NoOptionError:
        https_port = 443
    try:
        http_port = int(config.get('OpenMotics', 'http_port'))
    except NoOptionError:
        http_port = 80
    Injectable.value(https_port=https_port)
    Injectable.value(http_port=http_port)
    Injectable.value(ssl_private_key=constants.get_ssl_private_key_file())
    Injectable.value(ssl_certificate=constants.get_ssl_certificate_file())

    # TODO: Clean up dependencies more to reduce complexity

    # IOC announcements
    # When below modules are imported, the classes are registerd in the IOC graph. This is required for
    # instances that are used in @Inject decorated functions below, and is also needed to specify
    # abstract implementations depending on e.g. the platform (classic vs core) or certain settings (classic
    # thermostats vs gateway thermostats)
    from plugins import base
    from gateway import (metrics_controller, webservice, scheduling, observer,
                         gateway_api, metrics_collector,
                         maintenance_controller, user_controller,
                         pulse_counter_controller, metrics_caching, watchdog,
                         output_controller, room_controller, sensor_controller,
                         shutter_controller, group_action_controller,
                         module_controller, ventilation_controller)
    from cloud import events
    _ = (metrics_controller, webservice, scheduling, observer, gateway_api,
         metrics_collector, maintenance_controller, base, events,
         user_controller, pulse_counter_controller, metrics_caching, watchdog,
         output_controller, room_controller, sensor_controller,
         shutter_controller, group_action_controller, module_controller,
         ventilation_controller)

    # IPC
    message_client = None
    if message_client_name is not None:
        message_client = MessageClient(message_client_name)
    Injectable.value(message_client=message_client)

    # Cloud API
    Injectable.value(gateway_uuid=config.get('OpenMotics', 'uuid'))

    try:
        parsed_url = urlparse(config.get('OpenMotics', 'vpn_check_url'))
    except NoOptionError:
        parsed_url = urlparse('')
    Injectable.value(cloud_endpoint=parsed_url.hostname)
    Injectable.value(cloud_port=parsed_url.port)
    Injectable.value(cloud_ssl=parsed_url.scheme == 'https')
    Injectable.value(cloud_api_version=0)

    cloud_url = urlunparse(
        (parsed_url.scheme, parsed_url.netloc, '', '', '', ''))
    Injectable.value(cloud_url=cloud_url or None)

    try:
        firmware_url = config.get('OpenMotics', 'firmware_url')
    except NoOptionError:
        path = '/portal/firmware_metadata'
        firmware_url = urlunparse(
            (parsed_url.scheme, parsed_url.netloc, path, '', '', ''))
    Injectable.value(firmware_url=firmware_url or None)

    # User Controller
    Injectable.value(user_db=config_database_file)
    Injectable.value(user_db_lock=config_lock)
    Injectable.value(token_timeout=3600)
    Injectable.value(
        config={
            'username': config.get('OpenMotics', 'cloud_user'),
            'password': config.get('OpenMotics', 'cloud_pass')
        })

    # Metrics Controller
    Injectable.value(metrics_db=constants.get_metrics_database_file())
    Injectable.value(metrics_db_lock=metrics_lock)

    # Energy Controller
    try:
        power_serial_port = config.get('OpenMotics', 'power_serial')
    except NoOptionError:
        power_serial_port = ''
    if power_serial_port:
        Injectable.value(power_db=constants.get_power_database_file())
        Injectable.value(power_store=PowerStore())
        # TODO: make non blocking?
        Injectable.value(power_serial=RS485(
            Serial(power_serial_port, 115200, timeout=None)))
        Injectable.value(power_communicator=PowerCommunicator())
        Injectable.value(power_controller=PowerController())
        Injectable.value(p1_controller=P1Controller())
    else:
        Injectable.value(power_serial=None)
        Injectable.value(power_store=None)
        Injectable.value(
            power_communicator=None)  # TODO: remove from gateway_api
        Injectable.value(power_controller=None)
        Injectable.value(p1_controller=None)

    # Pulse Controller
    Injectable.value(pulse_db=constants.get_pulse_counter_database_file())

    # Master Controller
    try:
        controller_serial_port = config.get('OpenMotics', 'controller_serial')
    except NoOptionError:
        controller_serial_port = ''

    if controller_serial_port:
        Injectable.value(controller_serial=Serial(
            controller_serial_port, 115200, exclusive=True))
    if target_platform in [Platform.Type.DUMMY, Platform.Type.ESAFE]:
        Injectable.value(maintenance_communicator=None)
        Injectable.value(passthrough_service=None)
        Injectable.value(master_controller=MasterDummyController())
        Injectable.value(eeprom_db=None)
        from gateway.hal.master_controller_dummy import DummyEepromObject
        Injectable.value(eeprom_extension=DummyEepromObject())
    elif target_platform in Platform.CoreTypes:
        # FIXME don't create singleton for optional controller?
        from master.core import ucan_communicator, slave_communicator
        _ = ucan_communicator, slave_communicator
        core_cli_serial_port = config.get('OpenMotics', 'cli_serial')
        Injectable.value(cli_serial=Serial(core_cli_serial_port, 115200))
        Injectable.value(passthrough_service=None)  # Mark as "not needed"
        # TODO: Remove; should not be needed for Core
        Injectable.value(
            eeprom_db=constants.get_eeprom_extension_database_file())

        Injectable.value(master_communicator=CoreCommunicator())
        Injectable.value(
            maintenance_communicator=MaintenanceCoreCommunicator())
        Injectable.value(memory_file=MemoryFile())
        Injectable.value(master_controller=MasterCoreController())
    elif target_platform in Platform.ClassicTypes:
        # FIXME don't create singleton for optional controller?
        from master.classic import eeprom_extension
        _ = eeprom_extension
        leds_i2c_address = config.get('OpenMotics', 'leds_i2c_address')
        passthrough_serial_port = config.get('OpenMotics',
                                             'passthrough_serial')
        Injectable.value(
            eeprom_db=constants.get_eeprom_extension_database_file())
        Injectable.value(leds_i2c_address=int(leds_i2c_address, 16))
        if passthrough_serial_port:
            Injectable.value(
                passthrough_serial=Serial(passthrough_serial_port, 115200))
            from master.classic.passthrough import PassthroughService
            _ = PassthroughService  # IOC announcement
        else:
            Injectable.value(passthrough_service=None)
        Injectable.value(master_communicator=MasterCommunicator())
        Injectable.value(
            maintenance_communicator=MaintenanceClassicCommunicator())
        Injectable.value(master_controller=MasterClassicController())
    else:
        logger.warning('Unhandled master implementation for %s',
                       target_platform)

    if target_platform in [Platform.Type.DUMMY, Platform.Type.ESAFE]:
        Injectable.value(frontpanel_controller=None)
    elif target_platform in Platform.CoreTypes:
        Injectable.value(frontpanel_controller=FrontpanelCoreController())
    elif target_platform in Platform.ClassicTypes:
        Injectable.value(frontpanel_controller=FrontpanelClassicController())
    else:
        logger.warning('Unhandled frontpanel implementation for %s',
                       target_platform)

    # Thermostats
    thermostats_gateway_feature = Feature.get_or_none(
        name='thermostats_gateway')
    thermostats_gateway_enabled = thermostats_gateway_feature is not None and thermostats_gateway_feature.enabled
    if target_platform not in Platform.ClassicTypes or thermostats_gateway_enabled:
        Injectable.value(thermostat_controller=ThermostatControllerGateway())
    else:
        Injectable.value(thermostat_controller=ThermostatControllerMaster())
예제 #4
0
 def setUp(self):
     self.power_communicator = mock.Mock()
     SetUpTestInjections(power_communicator=self.power_communicator,
                         power_db=':memory:')
     self.store = PowerStore()
예제 #5
0
class PowerControllerTest(unittest.TestCase):
    """ Tests for PowerStore. """
    @classmethod
    def setUpClass(cls):
        SetTestMode()

    def setUp(self):
        self.power_communicator = mock.Mock()
        SetUpTestInjections(power_communicator=self.power_communicator,
                            power_db=':memory:')
        self.store = PowerStore()

    def test_empty(self):
        """ Test an empty database. """
        self.assertEqual({}, self.store.get_power_modules())
        self.assertEqual(1, self.store.get_free_address())

        self.store.register_power_module(1, POWER_MODULE)

        self.assertEqual(
            {
                1: {
                    'id': 1,
                    'address': 1,
                    'name': u'',
                    'version': 8,
                    'input0': u'',
                    'input1': u'',
                    'input2': u'',
                    'input3': u'',
                    'input4': u'',
                    'input5': u'',
                    'input6': u'',
                    'input7': u'',
                    'sensor0': 0,
                    'sensor1': 0,
                    'sensor2': 0,
                    'sensor3': 0,
                    'sensor4': 0,
                    'sensor5': 0,
                    'sensor6': 0,
                    'sensor7': 0,
                    'times0': None,
                    'times1': None,
                    'times2': None,
                    'times3': None,
                    'times4': None,
                    'times5': None,
                    'times6': None,
                    'times7': None,
                    'inverted0': 0,
                    'inverted1': 0,
                    'inverted2': 0,
                    'inverted3': 0,
                    'inverted4': 0,
                    'inverted5': 0,
                    'inverted6': 0,
                    'inverted7': 0
                }
            }, self.store.get_power_modules())

        self.assertEqual(2, self.store.get_free_address())

        self.store.register_power_module(5, POWER_MODULE)
        self.assertEqual(
            {
                1: {
                    'id': 1,
                    'address': 1,
                    'name': u'',
                    'version': 8,
                    'input0': u'',
                    'input1': u'',
                    'input2': u'',
                    'input3': u'',
                    'input4': u'',
                    'input5': u'',
                    'input6': u'',
                    'input7': u'',
                    'sensor0': 0,
                    'sensor1': 0,
                    'sensor2': 0,
                    'sensor3': 0,
                    'sensor4': 0,
                    'sensor5': 0,
                    'sensor6': 0,
                    'sensor7': 0,
                    'times0': None,
                    'times1': None,
                    'times2': None,
                    'times3': None,
                    'times4': None,
                    'times5': None,
                    'times6': None,
                    'times7': None,
                    'inverted0': 0,
                    'inverted1': 0,
                    'inverted2': 0,
                    'inverted3': 0,
                    'inverted4': 0,
                    'inverted5': 0,
                    'inverted6': 0,
                    'inverted7': 0
                },
                2: {
                    'id': 2,
                    'address': 5,
                    'name': u'',
                    'version': 8,
                    'input0': u'',
                    'input1': u'',
                    'input2': u'',
                    'input3': u'',
                    'input4': u'',
                    'input5': u'',
                    'input6': u'',
                    'input7': u'',
                    'sensor0': 0,
                    'sensor1': 0,
                    'sensor2': 0,
                    'sensor3': 0,
                    'sensor4': 0,
                    'sensor5': 0,
                    'sensor6': 0,
                    'sensor7': 0,
                    'times0': None,
                    'times1': None,
                    'times2': None,
                    'times3': None,
                    'times4': None,
                    'times5': None,
                    'times6': None,
                    'times7': None,
                    'inverted0': 0,
                    'inverted1': 0,
                    'inverted2': 0,
                    'inverted3': 0,
                    'inverted4': 0,
                    'inverted5': 0,
                    'inverted6': 0,
                    'inverted7': 0
                }
            }, self.store.get_power_modules())

        self.assertEqual(6, self.store.get_free_address())

    def test_update(self):
        """ Test for updating the power module information. """
        self.assertEqual({}, self.store.get_power_modules())

        self.store.register_power_module(1, POWER_MODULE)

        self.assertEqual(
            {
                1: {
                    'id': 1,
                    'address': 1,
                    'name': u'',
                    'version': 8,
                    'input0': u'',
                    'input1': u'',
                    'input2': u'',
                    'input3': u'',
                    'input4': u'',
                    'input5': u'',
                    'input6': u'',
                    'input7': u'',
                    'sensor0': 0,
                    'sensor1': 0,
                    'sensor2': 0,
                    'sensor3': 0,
                    'sensor4': 0,
                    'sensor5': 0,
                    'sensor6': 0,
                    'sensor7': 0,
                    'times0': None,
                    'times1': None,
                    'times2': None,
                    'times3': None,
                    'times4': None,
                    'times5': None,
                    'times6': None,
                    'times7': None,
                    'inverted0': 0,
                    'inverted1': 0,
                    'inverted2': 0,
                    'inverted3': 0,
                    'inverted4': 0,
                    'inverted5': 0,
                    'inverted6': 0,
                    'inverted7': 0
                }
            }, self.store.get_power_modules())

        times = ",".join(["00:00" for _ in range(14)])

        self.store.update_power_module({
            'id': 1,
            'name': 'module1',
            'input0': 'in0',
            'input1': 'in1',
            'input2': 'in2',
            'input3': 'in3',
            'input4': 'in4',
            'input5': 'in5',
            'input6': 'in6',
            'input7': 'in7',
            'sensor0': 0,
            'sensor1': 1,
            'sensor2': 2,
            'sensor3': 3,
            'sensor4': 4,
            'sensor5': 5,
            'sensor6': 6,
            'sensor7': 7,
            'times0': times,
            'times1': times,
            'times2': times,
            'times3': times,
            'times4': times,
            'times5': times,
            'times6': times,
            'times7': times,
            'inverted0': 0,
            'inverted1': 0,
            'inverted2': 0,
            'inverted3': 0,
            'inverted4': 0,
            'inverted5': 0,
            'inverted6': 0,
            'inverted7': 0
        })

        self.assertEqual(
            {
                1: {
                    'id': 1,
                    'address': 1,
                    'version': 8,
                    'name': 'module1',
                    'input0': 'in0',
                    'input1': 'in1',
                    'input2': 'in2',
                    'input3': 'in3',
                    'input4': 'in4',
                    'input5': 'in5',
                    'input6': 'in6',
                    'input7': 'in7',
                    'sensor0': 0,
                    'sensor1': 1,
                    'sensor2': 2,
                    'sensor3': 3,
                    'sensor4': 4,
                    'sensor5': 5,
                    'sensor6': 6,
                    'sensor7': 7,
                    'times0': times,
                    'times1': times,
                    'times2': times,
                    'times3': times,
                    'times4': times,
                    'times5': times,
                    'times6': times,
                    'times7': times,
                    'inverted0': 0,
                    'inverted1': 0,
                    'inverted2': 0,
                    'inverted3': 0,
                    'inverted4': 0,
                    'inverted5': 0,
                    'inverted6': 0,
                    'inverted7': 0
                }
            }, self.store.get_power_modules())

    def test_module_exists(self):
        """ Test for module_exists. """

        self.assertFalse(self.store.module_exists(1))

        self.store.register_power_module(1, POWER_MODULE)

        self.assertTrue(self.store.module_exists(1))
        self.assertFalse(self.store.module_exists(2))

    def test_readdress_power_module(self):
        """ Test for readdress_power_module. """
        self.store.register_power_module(1, POWER_MODULE)

        self.store.readdress_power_module(1, 2)

        self.assertFalse(self.store.module_exists(1))
        self.assertTrue(self.store.module_exists(2))

        self.assertEqual(
            {
                1: {
                    'id': 1,
                    'address': 2,
                    'name': u'',
                    'version': 8,
                    'input0': u'',
                    'input1': u'',
                    'input2': u'',
                    'input3': u'',
                    'input4': u'',
                    'input5': u'',
                    'input6': u'',
                    'input7': u'',
                    'sensor0': 0,
                    'sensor1': 0,
                    'sensor2': 0,
                    'sensor3': 0,
                    'sensor4': 0,
                    'sensor5': 0,
                    'sensor6': 0,
                    'sensor7': 0,
                    'times0': None,
                    'times1': None,
                    'times2': None,
                    'times3': None,
                    'times4': None,
                    'times5': None,
                    'times6': None,
                    'times7': None,
                    'inverted0': 0,
                    'inverted1': 0,
                    'inverted2': 0,
                    'inverted3': 0,
                    'inverted4': 0,
                    'inverted5': 0,
                    'inverted6': 0,
                    'inverted7': 0
                }
            }, self.store.get_power_modules())

    def test_get_address(self):
        """ Test for get_address. """
        self.assertEqual({}, self.store.get_power_modules())

        self.store.register_power_module(1, POWER_MODULE)
        self.store.readdress_power_module(1, 3)

        self.assertEqual(3, self.store.get_address(1))
예제 #6
0
class PowerCommunicatorTest(unittest.TestCase):
    """ Tests for PowerCommunicator class """

    @classmethod
    def setUpClass(cls):
        SetTestMode()

    def setUp(self):
        self.pubsub = PubSub()
        SetUpTestInjections(pubsub=self.pubsub)
        self.power_data = []  # type: list
        SetUpTestInjections(power_db=':memory:')
        self.serial = RS485(SerialMock(self.power_data))
        self.store = PowerStore()
        SetUpTestInjections(power_serial=self.serial,
                            power_store=self.store)
        self.communicator = PowerCommunicator()

    def tearDown(self):
        self.communicator.stop()
        self.serial.stop()

    def test_do_command(self):
        """ Test for standard behavior PowerCommunicator.do_command. """
        action = power_api.get_voltage(power_api.POWER_MODULE)

        self.power_data.extend([
            sin(action.create_input(1, 1)), sout(action.create_output(1, 1, 49.5))
        ])
        self.serial.start()
        self.communicator.start()

        output = self.communicator.do_command(1, action)
        self.assertEqual((49.5, ), output)

        self.assertEqual(14, self.communicator.get_communication_statistics()['bytes_written'])
        self.assertEqual(18, self.communicator.get_communication_statistics()['bytes_read'])

    def test_do_command_timeout_once(self):
        """ Test for timeout in PowerCommunicator.do_command. """
        action = power_api.get_voltage(power_api.POWER_MODULE)

        self.power_data.extend([
            sin(action.create_input(1, 1)),
            sout(bytearray()),
            sin(action.create_input(1, 2)),
            sout(action.create_output(1, 2, 49.5))
        ])
        self.serial.start()
        self.communicator.start()

        output = self.communicator.do_command(1, action)
        self.assertEqual((49.5, ), output)

    def test_do_command_timeout_twice(self):
        """ Test for timeout in PowerCommunicator.do_command. """
        action = power_api.get_voltage(power_api.POWER_MODULE)

        self.power_data.extend([
            sin(action.create_input(1, 1)),
            sout(bytearray()),
            sin(action.create_input(1, 2)),
            sout(bytearray())
        ])
        self.serial.start()
        self.communicator.start()

        with self.assertRaises(CommunicationTimedOutException):
            self.communicator.do_command(1, action)

    def test_do_command_split_data(self):
        """ Test PowerCommunicator.do_command when the data is split over multiple reads. """
        action = power_api.get_voltage(power_api.POWER_MODULE)
        out = action.create_output(1, 1, 49.5)

        self.power_data.extend([
            sin(action.create_input(1, 1)),
            sout(out[:5]), sout(out[5:])
        ])
        self.serial.start()
        self.communicator.start()

        output = self.communicator.do_command(1, action)
        self.assertEqual((49.5, ), output)

    def test_wrong_response(self):
        """ Test PowerCommunicator.do_command when the power module returns a wrong response. """
        action_1 = power_api.get_voltage(power_api.POWER_MODULE)
        action_2 = power_api.get_frequency(power_api.POWER_MODULE)

        self.power_data.extend([
            sin(action_1.create_input(1, 1)),
            sout(action_2.create_output(3, 2, 49.5))
        ])
        self.serial.start()
        self.communicator.start()

        with self.assertRaises(Exception):
            self.communicator.do_command(1, action_1)

    @mark.slow
    def test_address_mode(self):
        """ Test the address mode. """
        events = []

        def handle_events(master_event):
            events.append(master_event)

        self.pubsub.subscribe_master_events(PubSub.MasterTopics.POWER, handle_events)

        sad = power_api.set_addressmode(power_api.POWER_MODULE)
        sad_p1c = power_api.set_addressmode(power_api.P1_CONCENTRATOR)

        self.power_data.extend([
            sin(sad.create_input(power_api.BROADCAST_ADDRESS, 1, power_api.ADDRESS_MODE)),
            sin(sad_p1c.create_input(power_api.BROADCAST_ADDRESS, 2, power_api.ADDRESS_MODE)),
            sout(power_api.want_an_address(power_api.POWER_MODULE).create_output(0, 0)),
            sin(power_api.set_address(power_api.POWER_MODULE).create_input(0, 0, 1)),
            sout(power_api.want_an_address(power_api.ENERGY_MODULE).create_output(0, 0)),
            sin(power_api.set_address(power_api.ENERGY_MODULE).create_input(0, 0, 2)),
            sout(power_api.want_an_address(power_api.P1_CONCENTRATOR).create_output(0, 0)),
            sin(power_api.set_address(power_api.P1_CONCENTRATOR).create_input(0, 0, 3)),
            sout(bytearray()),  # Timeout read after 1 second
            sin(sad.create_input(power_api.BROADCAST_ADDRESS, 3, power_api.NORMAL_MODE)),
            sin(sad_p1c.create_input(power_api.BROADCAST_ADDRESS, 4, power_api.NORMAL_MODE))
        ])
        self.serial.start()
        self.communicator.start()

        self.assertEqual(self.store.get_free_address(), 1)

        self.communicator.start_address_mode()
        self.assertTrue(self.communicator.in_address_mode())
        self.pubsub._publish_all_events()
        time.sleep(0.5)
        assert [] == events

        self.communicator.stop_address_mode()
        self.pubsub._publish_all_events()
        assert MasterEvent(MasterEvent.Types.POWER_ADDRESS_EXIT, {}) in events
        assert len(events) == 1

        self.assertEqual(self.store.get_free_address(), 4)
        self.assertFalse(self.communicator.in_address_mode())


    @mark.slow
    def test_do_command_in_address_mode(self):
        """ Test the behavior of do_command in address mode."""
        action = power_api.get_voltage(power_api.POWER_MODULE)
        sad = power_api.set_addressmode(power_api.POWER_MODULE)
        sad_p1c = power_api.set_addressmode(power_api.P1_CONCENTRATOR)

        self.power_data.extend([
            sin(sad.create_input(power_api.BROADCAST_ADDRESS, 1, power_api.ADDRESS_MODE)),
            sin(sad_p1c.create_input(power_api.BROADCAST_ADDRESS, 2, power_api.ADDRESS_MODE)),
            sout(bytearray()),  # Timeout read after 1 second
            sin(sad.create_input(power_api.BROADCAST_ADDRESS, 3, power_api.NORMAL_MODE)),
            sin(sad_p1c.create_input(power_api.BROADCAST_ADDRESS, 4, power_api.NORMAL_MODE)),
            sin(action.create_input(1, 5)),
            sout(action.create_output(1, 5, 49.5))
        ])
        self.serial.start()
        self.communicator.start()

        self.communicator.start_address_mode()
        with self.assertRaises(InAddressModeException):
            self.communicator.do_command(1, action)

        self.communicator.stop_address_mode()
        self.assertEqual((49.5, ), self.communicator.do_command(1, action))

    @mark.slow
    def test_address_mode_timeout(self):
        """ Test address mode timeout. """
        action = power_api.get_voltage(power_api.POWER_MODULE)
        sad = power_api.set_addressmode(power_api.POWER_MODULE)
        sad_p1c = power_api.set_addressmode(power_api.P1_CONCENTRATOR)

        self.power_data.extend([
            sin(sad.create_input(power_api.BROADCAST_ADDRESS, 1, power_api.ADDRESS_MODE)),
            sin(sad_p1c.create_input(power_api.BROADCAST_ADDRESS, 2, power_api.ADDRESS_MODE)),
            sout(bytearray()),  # Timeout read after 1 second
            sin(sad.create_input(power_api.BROADCAST_ADDRESS, 3, power_api.NORMAL_MODE)),
            sin(sad_p1c.create_input(power_api.BROADCAST_ADDRESS, 4, power_api.NORMAL_MODE)),
            sin(action.create_input(1, 5)),
            sout(action.create_output(1, 5, 49.5))
        ])
        self.communicator = PowerCommunicator(address_mode_timeout=1)
        self.serial.start()
        self.communicator.start()

        self.communicator.start_address_mode()
        time.sleep(1.1)

        self.assertEqual((49.5, ), self.communicator.do_command(1, action))

    @mark.slow
    def test_timekeeper(self):
        """ Test the TimeKeeper. """
        self.store.register_power_module(1, power_api.POWER_MODULE)

        time_action = power_api.set_day_night(power_api.POWER_MODULE)
        times = [power_api.NIGHT for _ in range(8)]
        action = power_api.get_voltage(power_api.POWER_MODULE)

        self.power_data.extend([
            sin(time_action.create_input(1, 1, *times)),
            sout(time_action.create_output(1, 1)),
            sin(action.create_input(1, 2)),
            sout(action.create_output(1, 2, 243))
        ])
        self.communicator = PowerCommunicator(time_keeper_period=1)
        self.serial.start()
        self.communicator.start()

        time.sleep(1.5)
        self.assertEqual((243, ), self.communicator.do_command(1, action))