diff --git a/README.rst b/README.rst index 476f57f4..d2c9cb92 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ This library has been written with reference to the following documents: - IEC 62386-102:2014 (general requirements for control gear) - IEC 62386-103:2014 (general requirements for control devices) - IEC 62386-201:2009 (fluorescent lamps) -- IEC 62386-202:2009 (self-contained emergency lighting) +- IEC 62386-202:2022 (self-contained emergency lighting) - IEC 62386-205:2009 (supply voltage controller for incandescent lamps) - IEC 62386-206:2009 (conversion from digital signal into DC voltage) - IEC 62386-207:2009 (LED modules) diff --git a/dali/gear/sequences.py b/dali/gear/sequences.py index e087e7de..23d42291 100644 --- a/dali/gear/sequences.py +++ b/dali/gear/sequences.py @@ -3,10 +3,12 @@ """ from __future__ import annotations +from dataclasses import dataclass from typing import Generator, Optional from dali import command from dali.address import GearAddress, GearShort +from dali.exceptions import DALISequenceError from dali.gear.colour import ( Activate, QueryColourValue, @@ -14,6 +16,22 @@ SetTemporaryColourTemperature, StoreColourTemperatureTcLimit, ) +from dali.gear.emergency import ( + QueryBatteryCharge, + QueryDurationTestResult, + QueryEmergencyFailureStatus, + QueryEmergencyFailureStatusResponse, + QueryEmergencyFeatures, + QueryEmergencyFeaturesResponse, + QueryEmergencyLevel, + QueryEmergencyMode, + QueryEmergencyModeResponse, + QueryEmergencyStatus, + QueryEmergencyStatusResponse, + QueryLampEmergencyTime, + QueryLampTotalOperationTime, + QueryRatedDuration, +) from dali.gear.general import DTR0, DTR1, DTR2, QueryActualLevel, QueryContentDTR0 @@ -112,3 +130,97 @@ def SetDT8TcLimit( yield DTR1(tc_bytes[1]) yield DTR2(what_limit) yield StoreColourTemperatureTcLimit(address) + + +@dataclass(frozen=True) +class EmergencyInformation: + """DT1 (IEC 62386-202) status and measurement values of an emergency + control gear. + + Each field holds the query's response object, preserving the attributes + those classes decode: the named status bits (for example + ``emergency_mode.emergency_mode``) and the measurement values + (``battery_charge.value``, which is an integer or ``"MASK"``). + """ + + emergency_mode: QueryEmergencyModeResponse + emergency_features: QueryEmergencyFeaturesResponse + emergency_failure_status: QueryEmergencyFailureStatusResponse + emergency_status: QueryEmergencyStatusResponse + battery_charge: command.NumericResponseMask + emergency_level: command.NumericResponseMask + duration_test_result: command.NumericResponse + lamp_emergency_time: command.NumericResponse + lamp_total_operation_time: command.NumericResponse + rated_duration: command.NumericResponse + + +def _require(response: command.Response, what: str) -> command.Response: + """Return the response, or abort the sequence if the gear did not answer + or replied with a framing error.""" + raw = response.raw_value + if raw is None: + raise DALISequenceError(f"No response to {what}") + if raw.error: + raise DALISequenceError(f"Framing error in response to {what}") + return response + + +def QueryEmergencyInformation( + address: GearShort, +) -> Generator[command.Command, Optional[command.Response], EmergencyInformation]: + """Read the DT1 (IEC 62386-202) status and measurement values of an + emergency control gear in a single sequence. + + Bundling the queries into one sequence runs them under a single + transaction, so no other command can interleave between them. Each query's + response object is kept in the returned EmergencyInformation, so the caller + can read the named status bits and measurement values the response classes + decode. + + A MASK measurement is a valid answer and is preserved; the sequence aborts + with a DALISequenceError if any query goes unanswered or returns a framing + error. + + :param address: GearShort address to query + :return: an EmergencyInformation dataclass + """ + # Although the proper types are expected, ints are common enough for + # addresses and their meaning is unambiguous in this context + if isinstance(address, int): + address = GearShort(address) + + return EmergencyInformation( + emergency_mode=_require( + (yield QueryEmergencyMode(address)), "QueryEmergencyMode" + ), + emergency_features=_require( + (yield QueryEmergencyFeatures(address)), "QueryEmergencyFeatures" + ), + emergency_failure_status=_require( + (yield QueryEmergencyFailureStatus(address)), + "QueryEmergencyFailureStatus", + ), + emergency_status=_require( + (yield QueryEmergencyStatus(address)), "QueryEmergencyStatus" + ), + battery_charge=_require( + (yield QueryBatteryCharge(address)), "QueryBatteryCharge" + ), + emergency_level=_require( + (yield QueryEmergencyLevel(address)), "QueryEmergencyLevel" + ), + duration_test_result=_require( + (yield QueryDurationTestResult(address)), "QueryDurationTestResult" + ), + lamp_emergency_time=_require( + (yield QueryLampEmergencyTime(address)), "QueryLampEmergencyTime" + ), + lamp_total_operation_time=_require( + (yield QueryLampTotalOperationTime(address)), + "QueryLampTotalOperationTime", + ), + rated_duration=_require( + (yield QueryRatedDuration(address)), "QueryRatedDuration" + ), + ) diff --git a/dali/memory/__init__.py b/dali/memory/__init__.py index 954283df..c9591db0 100644 --- a/dali/memory/__init__.py +++ b/dali/memory/__init__.py @@ -5,4 +5,5 @@ 'energy', 'diagnostics', 'maintenance', + 'emergency', ] diff --git a/dali/memory/emergency.py b/dali/memory/emergency.py new file mode 100644 index 00000000..3512f740 --- /dev/null +++ b/dali/memory/emergency.py @@ -0,0 +1,334 @@ +"""Memory bank 208 from IEC 62386-202 (self-contained emergency lighting). + +Section 9.15.6, Table 18: information about the control gear, lamp and +battery of a device-type-1 (emergency) control gear. +""" +from .location import ( + FixedScaleNumericValue, + MemoryBank, + MemoryLocation, + MemoryRange, + MemoryType, + NumericValue, + TemperatureValue, +) + +# Table 18 lists 0x32 as the default last addressable location but also +# defines a value at 0x33 ("Performed on this battery"); declare the bank +# up to 0x33 so that value is treated as addressable when a device reports +# it. read_all() always honours the device's own reported last address. +BANK_208 = MemoryBank(208, 0x33, has_lock=True, has_latch=True) + + +class MemoryBankVersion(NumericValue): + """Version of memory bank 208.""" + bank = BANK_208 + locations = MemoryLocation(address=0x03, default=0x01, type_=MemoryType.ROM) + + +class ControlGearMaxReferenceTemperature(TemperatureValue): + """Control gear maximum reference temperature, typically at the Tc point.""" + bank = BANK_208 + locations = MemoryLocation(address=0x04, type_=MemoryType.ROM) + mask_supported = True + min_value = 0 + max_value = 0xFD + + +class ControlGearTemperature(TemperatureValue): + """Internal temperature of the control gear.""" + bank = BANK_208 + locations = MemoryLocation(address=0x05, type_=MemoryType.RAM_RO) + mask_supported = True + tmask_supported = True + min_value = 0 + max_value = 0xFD + + +class MinControlGearTemperatureTotal(TemperatureValue): + """Minimum measured gear temperature (total).""" + bank = BANK_208 + locations = MemoryLocation(address=0x06, type_=MemoryType.NVM_RO) + mask_supported = True + tmask_supported = True + min_value = 0 + max_value = 0xFD + + +class MaxControlGearTemperatureTotal(TemperatureValue): + """Maximum measured gear temperature (total).""" + bank = BANK_208 + locations = MemoryLocation(address=0x07, type_=MemoryType.NVM_RO) + mask_supported = True + tmask_supported = True + min_value = 0 + max_value = 0xFD + + +class MinControlGearTemperatureCurrentBattery(TemperatureValue): + """Minimum measured gear temperature (current battery).""" + bank = BANK_208 + locations = MemoryLocation(address=0x08, type_=MemoryType.NVM_RO) + mask_supported = True + tmask_supported = True + min_value = 0 + max_value = 0xFD + + +class MaxControlGearTemperatureCurrentBattery(TemperatureValue): + """Maximum measured gear temperature (current battery).""" + bank = BANK_208 + locations = MemoryLocation(address=0x09, type_=MemoryType.NVM_RO) + mask_supported = True + tmask_supported = True + min_value = 0 + max_value = 0xFD + + +class AveragePowerDuringCharging(FixedScaleNumericValue): + """Average power during battery charging; 0xFD means 25.3 W or greater.""" + bank = BANK_208 + unit = 'W' + scaling_factor = 0.1 + locations = MemoryLocation(address=0x0A, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class AveragePowerDuringMaintenance(FixedScaleNumericValue): + """Average power during battery charge maintenance; 0xFD means 25.3 W or + greater.""" + bank = BANK_208 + unit = 'W' + scaling_factor = 0.1 + locations = MemoryLocation(address=0x0B, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class RatedDuration(FixedScaleNumericValue): + """Rated duration time in minutes (stored in units of 2 min).""" + bank = BANK_208 + unit = 'min' + scaling_factor = 2 + locations = MemoryLocation(address=0x0C, type_=MemoryType.NVM_RO) + + +class FunctionTestTime(NumericValue): + """Nominal function test time in seconds; 0xFD means 253 s or greater.""" + bank = BANK_208 + unit = 's' + locations = MemoryLocation(address=0x0D, type_=MemoryType.NVM_RO) + min_value = 5 + max_value = 0xFD + + +class BatteryRechargeTime(FixedScaleNumericValue): + """Nominal battery recharge time in minutes (stored in units of 10 min); + 0xFD means 2530 min or greater.""" + bank = BANK_208 + unit = 'min' + scaling_factor = 10 + locations = MemoryLocation(address=0x0E, type_=MemoryType.NVM_RO) + min_value = 1 + max_value = 0xFD + + +class BatteryFailureCounter(NumericValue): + """Battery failure counter.""" + bank = BANK_208 + locations = MemoryLocation(address=0x0F, type_=MemoryType.NVM_RO) + mask_supported = True + max_value = 0xFD + + +class BatteryCutOffCounter(NumericValue): + """Count of transitions from below to above battery cut-off voltage; + 0xFD means 253 or greater.""" + bank = BANK_208 + locations = MemoryLocation(address=0x10, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class LampCutOffCounterTotal(NumericValue): + """Count of transitions from above to below lamp cut-off voltage (total).""" + bank = BANK_208 + locations = MemoryLocation(address=0x11, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class LampCutOffCounterCurrentBattery(NumericValue): + """Count of transitions from above to below lamp cut-off voltage + (current battery).""" + bank = BANK_208 + locations = MemoryLocation(address=0x12, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class LampEmergencyTimeTotal(NumericValue): + """Lamp emergency time (total) in minutes; 0xFFFFFD means 16777213 min or + greater.""" + bank = BANK_208 + unit = 'min' + locations = MemoryRange(start=0x13, end=0x15, type_=MemoryType.NVM_RO) + max_value = 0xFFFFFD + + +class LampEmergencyTimeCurrentBattery(NumericValue): + """Lamp emergency time (current battery) in minutes; 0xFFFFFD means + 16777213 min or greater.""" + bank = BANK_208 + unit = 'min' + locations = MemoryRange(start=0x16, end=0x18, type_=MemoryType.NVM_RO) + max_value = 0xFFFFFD + + +class BatteryConnectedTimeTotal(NumericValue): + """Battery connected time (total) in days; 0xFFFD means 65533 days or + greater.""" + bank = BANK_208 + unit = 'd' + locations = MemoryRange(start=0x19, end=0x1A, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class BatteryConnectedTimeCurrentBattery(NumericValue): + """Battery connected time (current battery) in days; 0xFFFD means 65533 + days or greater.""" + bank = BANK_208 + unit = 'd' + locations = MemoryRange(start=0x1B, end=0x1C, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class CircuitFailureCounter(NumericValue): + """Counts transitions of failure status bit 0 (circuit failure).""" + bank = BANK_208 + locations = MemoryLocation(address=0x1D, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class BatteryDurationFailureCounter(NumericValue): + """Counts transitions of failure status bit 1 (battery duration failure).""" + bank = BANK_208 + locations = MemoryLocation(address=0x1E, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class BatteryFailureStatusCounter(NumericValue): + """Counts transitions of failure status bit 2 (battery failure).""" + bank = BANK_208 + locations = MemoryLocation(address=0x1F, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class EmergencyLampFailureCounter(NumericValue): + """Counts transitions of failure status bit 3 (emergency lamp failure).""" + bank = BANK_208 + locations = MemoryLocation(address=0x20, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class FunctionTestMaxDelayExceededCounter(NumericValue): + """Counts transitions of failure status bit 4.""" + bank = BANK_208 + locations = MemoryLocation(address=0x21, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class DurationTestMaxDelayExceededCounter(NumericValue): + """Counts transitions of failure status bit 5.""" + bank = BANK_208 + locations = MemoryLocation(address=0x22, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class FunctionTestFailedCounterTotal(NumericValue): + """Counts transitions of failure status bit 6 (total); 0xFFFD means 65533 + or greater.""" + bank = BANK_208 + locations = MemoryRange(start=0x23, end=0x24, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class DurationTestFailedCounterTotal(NumericValue): + """Counts transitions of failure status bit 7 (total); 0xFD means 253 or + greater.""" + bank = BANK_208 + locations = MemoryLocation(address=0x25, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class FunctionTestFailedCounterCurrentBattery(NumericValue): + """Counts transitions of failure status bit 6 (current battery); 0xFFFD + means 65533 or greater.""" + bank = BANK_208 + locations = MemoryRange(start=0x26, end=0x27, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class DurationTestFailedCounterCurrentBattery(NumericValue): + """Counts transitions of failure status bit 7 (current battery); 0xFD means + 253 or greater.""" + bank = BANK_208 + locations = MemoryLocation(address=0x28, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class StartFunctionTestCounter(NumericValue): + """Counts transitions into function test mode; 0xFFFD means 65533 or + greater.""" + bank = BANK_208 + locations = MemoryRange(start=0x29, end=0x2A, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class StartDurationTestCounterTotal(NumericValue): + """Counts transitions into duration test mode (total); 0xFD means 253 or + greater.""" + bank = BANK_208 + locations = MemoryLocation(address=0x2B, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class StartDurationTestCounterCurrentBattery(NumericValue): + """Counts transitions into duration test mode (current battery); 0xFD means + 253 or greater.""" + bank = BANK_208 + locations = MemoryLocation(address=0x2C, type_=MemoryType.NVM_RO) + max_value = 0xFD + + +class RestModeCounter(NumericValue): + """Counts transitions into rest mode; 0xFFFD means 65533 or greater.""" + bank = BANK_208 + locations = MemoryRange(start=0x2D, end=0x2E, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class EmergencyModeCounterTotal(NumericValue): + """Counts transitions into emergency mode (total); 0xFFFD means 65533 or + greater.""" + bank = BANK_208 + locations = MemoryRange(start=0x2F, end=0x30, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class EmergencyModeCounterCurrentBattery(NumericValue): + """Counts transitions into emergency mode (current battery); 0xFFFD means + 65533 or greater.""" + bank = BANK_208 + locations = MemoryRange(start=0x31, end=0x32, type_=MemoryType.NVM_RO) + max_value = 0xFFFD + + +class PerformedOnThisBattery(NumericValue): + """Bitmap of modes/tests that occurred on the current battery. + + Bit 0: rest mode was active at least once + Bit 1: battery is successfully charging or maintained at full charge + Bit 2: emergency mode was active at least once + Bit 3: extended emergency mode was active at least once + Bit 4: function test was active at least once + Bit 5: duration test was active at least once + """ + bank = BANK_208 + locations = MemoryLocation(address=0x33, type_=MemoryType.RAM_RO) diff --git a/dali/tests/fakes.py b/dali/tests/fakes.py index 3885cccf..a410501c 100644 --- a/dali/tests/fakes.py +++ b/dali/tests/fakes.py @@ -130,6 +130,19 @@ def __init__(self, shortaddr=None, groups=set(), self.ct_mired_min = 153 self.ct_mired_max = 370 self.physical_minimum = 1 + # DT1 (IEC 62386-202) emergency lighting state; only used when 1 is + # in devicetypes. Defaults describe a healthy type-A gear in normal + # mode with a fully charged battery. + self.emergency_mode = 0b00000010 # normal mode + self.emergency_features = 0b00000011 # integral + maintained (type A) + self.emergency_failure_status = 0x00 + self.emergency_status = 0b00001000 # battery fully charged + self.battery_charge = 254 + self.emergency_level = 254 + self.duration_test_result = 60 + self.lamp_emergency_time = 5 + self.lamp_total_operation_time = 100 + self.rated_duration = 90 self.memory_banks = {} for fake_bank in memory_banks: bank_number = fake_bank.bank.address @@ -438,6 +451,43 @@ def send(self, cmd): elif isinstance(cmd, gear.colour.QueryExtendedVersionNumber): return 2 + # Handle DT1 emergency lighting commands; device type decoding has + # already been handled + if isinstance(cmd, gear.emergency._EmergencyLightingCommand): + # Only handle if this Gear instance has been declared as a DT1 type + if 1 not in self.devicetypes: + return + if isinstance(cmd, gear.emergency.QueryEmergencyMode): + return self.emergency_mode + elif isinstance(cmd, gear.emergency.QueryEmergencyFeatures): + return self.emergency_features + elif isinstance(cmd, gear.emergency.QueryEmergencyFailureStatus): + return self.emergency_failure_status + elif isinstance(cmd, gear.emergency.QueryEmergencyStatus): + return self.emergency_status + elif isinstance(cmd, gear.emergency.QueryBatteryCharge): + return self.battery_charge + elif isinstance(cmd, gear.emergency.QueryEmergencyLevel): + return self.emergency_level + elif isinstance(cmd, gear.emergency.QueryDurationTestResult): + return self.duration_test_result + elif isinstance(cmd, gear.emergency.QueryLampEmergencyTime): + return self.lamp_emergency_time + elif isinstance(cmd, gear.emergency.QueryLampTotalOperationTime): + return self.lamp_total_operation_time + elif isinstance(cmd, gear.emergency.QueryRatedDuration): + return self.rated_duration + elif isinstance(cmd, gear.emergency.StartFunctionTest): + self.emergency_status |= 0b00010000 # function test pending + elif isinstance(cmd, gear.emergency.StartDurationTest): + self.emergency_status |= 0b00100000 # duration test pending + elif isinstance(cmd, gear.emergency.StopTest): + self.emergency_status &= ~0b00110000 # clear both pending bits + elif isinstance(cmd, gear.emergency.Inhibit): + self.emergency_status |= 0b00000001 # inhibit mode + elif isinstance(cmd, gear.emergency.ReLightResetInhibit): + self.emergency_status &= ~0b00000001 + # Valid bank 0 ROM contents compatible with IEC 62386-103 class FakeDeviceBank0(FakeMemoryBank): diff --git a/dali/tests/test_emergency.py b/dali/tests/test_emergency.py new file mode 100644 index 00000000..80ab2da6 --- /dev/null +++ b/dali/tests/test_emergency.py @@ -0,0 +1,71 @@ +import pytest + +from dali.exceptions import DALISequenceError +from dali.frame import BackwardFrame +from dali.gear.emergency import ( + QueryEmergencyFailureStatusResponse, + QueryEmergencyModeResponse, + QueryEmergencyStatusResponse, +) +from dali.gear.sequences import QueryEmergencyInformation +from dali.tests import fakes + + +def test_emergency_status_decoding(): + rsp = QueryEmergencyStatusResponse(BackwardFrame(0b00001010)) + assert rsp.inhibit_mode == 0 + assert rsp.function_test_done_and_result_valid == 1 + assert rsp.duration_test_done_and_result_valid == 0 + assert rsp.battery_fully_charged == 1 + assert set(rsp.status) == { + "function test done and result valid", + "battery fully charged", + } + + +def test_emergency_failure_decoding(): + rsp = QueryEmergencyFailureStatusResponse(BackwardFrame(0b00000101)) + assert rsp.circuit_failure == 1 + assert rsp.battery_duration_failure == 0 + assert rsp.battery_failure == 1 + + +def test_emergency_mode_property(): + # Only the "emergency mode" bit set + rsp = QueryEmergencyModeResponse(BackwardFrame(0b00000100)) + assert rsp.mode == "emergency mode" + + +def test_query_emergency_information(): + bus = fakes.Bus([fakes.Gear(shortaddr=0, devicetypes=[1])]) + info = bus.run_sequence(QueryEmergencyInformation(0)) + assert info.emergency_mode.normal_mode == 1 + assert info.emergency_mode.emergency_mode == 0 + assert info.emergency_features.integral_emergency_control_gear == 1 + assert info.emergency_features.maintained_control_gear == 1 + assert info.emergency_features.switched_maintained_control_gear == 0 + assert info.emergency_failure_status.status == [] + assert info.emergency_status.battery_fully_charged == 1 + assert info.emergency_status.inhibit_mode == 0 + assert info.battery_charge.value == 254 + assert info.emergency_level.value == 254 + assert info.duration_test_result.value == 60 + assert info.lamp_emergency_time.value == 5 + assert info.lamp_total_operation_time.value == 100 + assert info.rated_duration.value == 90 + + +def test_query_emergency_information_mask(): + gear = fakes.Gear(shortaddr=0, devicetypes=[1]) + gear.battery_charge = 255 # MASK + bus = fakes.Bus([gear]) + info = bus.run_sequence(QueryEmergencyInformation(0)) + assert info.battery_charge.value == "MASK" + + +def test_query_emergency_information_aborts_on_missing_response(): + gear = fakes.Gear(shortaddr=0, devicetypes=[1]) + gear.emergency_mode = None # gear does not answer the first query + bus = fakes.Bus([gear]) + with pytest.raises(DALISequenceError): + bus.run_sequence(QueryEmergencyInformation(0)) diff --git a/dali/tests/test_memory.py b/dali/tests/test_memory.py index 4cc0dc31..47dae877 100644 --- a/dali/tests/test_memory.py +++ b/dali/tests/test_memory.py @@ -12,7 +12,7 @@ ) from dali.frame import BackwardFrame from dali.gear.general import DTR0, DTR1, ReadMemoryLocation -from dali.memory import diagnostics, energy, info, maintenance, oem +from dali.memory import diagnostics, emergency, energy, info, maintenance, oem from dali.memory.location import FlagValue, MemoryBank, NumericValue from dali.tests import fakes @@ -157,6 +157,51 @@ class FakeBank207(fakes.FakeMemoryBank): ] +# Self-contained emergency control gear information (IEC 62386-202) +class FakeBank208(fakes.FakeMemoryBank): + bank = emergency.BANK_208 + initial_contents = [ + 0x33, None, 0xff, + 0x01, # Version 1 + 85, # Max reference temperature 25°C + 90, # Control gear temperature 30°C + 50, # Min gear temperature (total) -10°C + 100, # Max gear temperature (total) 40°C + 60, # Min gear temperature (current battery) 0°C + 95, # Max gear temperature (current battery) 35°C + 100, # Average power during charging 10.0W + 50, # Average power during maintenance 5.0W + 90, # Rated duration 180 min + 10, # Function test time 10s + 12, # Battery recharge time 120 min + 0, # Battery failure counter + 3, # Battery cut-off counter + 1, # Lamp cut-off counter (total) + 0, # Lamp cut-off counter (current battery) + 0x00, 0x03, 0xe8, # Lamp emergency time (total) 1000 min + 0x00, 0x01, 0xf4, # Lamp emergency time (current battery) 500 min + 0x01, 0x6d, # Battery connected time (total) 365 days + 0x00, 0x1e, # Battery connected time (current battery) 30 days + 0, # Circuit failure counter + 1, # Battery duration failure counter + 2, # Battery failure status counter + 0, # Emergency lamp failure counter + 0, # Function test max delay exceeded counter + 0, # Duration test max delay exceeded counter + 0x00, 0x05, # Function test failed counter (total) 5 + 1, # Duration test failed counter (total) + 0x00, 0x02, # Function test failed counter (current battery) 2 + 0, # Duration test failed counter (current battery) + 0x00, 0x32, # Start function test counter 50 + 10, # Start duration test counter (total) + 3, # Start duration test counter (current battery) + 0x00, 0x07, # Rest mode counter 7 + 0x00, 0x14, # Emergency mode counter (total) 20 + 0x00, 0x04, # Emergency mode counter (current battery) 4 + 0x0a, # Performed on this battery bitmap + ] + + # Invalid / special memory bank contents class InvalidBank1(fakes.FakeMemoryBank): """Strange version of bank 1 @@ -314,7 +359,8 @@ def setUp(self): self.bus = fakes.Bus([ fakes.Gear(GearShort(0), memory_banks=( fakes.FakeBank0, FakeBank1, FakeBank202, FakeBank203, - FakeBank204, FakeBank205, FakeBank206, FakeBank207)), + FakeBank204, FakeBank205, FakeBank206, FakeBank207, + FakeBank208)), fakes.Gear(GearShort(1), memory_banks=( fakes.FakeBank0, InvalidBank1, InvalidBank202, LatchTestBank203, InvalidBank207)), @@ -542,6 +588,35 @@ def test_invalid_oem_strings(self): self.assertEqual(self.bus.run_sequence( oem.LuminaireIdentification.read(0)), FlagValue.Invalid) + def test_emergency(self): + self._test_value(emergency.MemoryBankVersion, 1) + self._test_value(emergency.ControlGearMaxReferenceTemperature, 25) + self._test_value(emergency.ControlGearTemperature, 30) + self._test_value(emergency.MinControlGearTemperatureTotal, -10) + self._test_value(emergency.MaxControlGearTemperatureTotal, 40) + self._test_value(emergency.AveragePowerDuringCharging, 10.0) + self._test_value(emergency.AveragePowerDuringMaintenance, 5.0) + self._test_value(emergency.RatedDuration, 180) + self._test_value(emergency.FunctionTestTime, 10) + self._test_value(emergency.BatteryRechargeTime, 120) + self._test_value(emergency.BatteryCutOffCounter, 3) + self._test_value(emergency.LampEmergencyTimeTotal, 1000) + self._test_value(emergency.LampEmergencyTimeCurrentBattery, 500) + self._test_value(emergency.BatteryConnectedTimeTotal, 365) + self._test_value(emergency.BatteryDurationFailureCounter, 1) + self._test_value(emergency.BatteryFailureStatusCounter, 2) + self._test_value(emergency.FunctionTestFailedCounterTotal, 5) + self._test_value(emergency.StartFunctionTestCounter, 50) + self._test_value(emergency.EmergencyModeCounterTotal, 20) + self._test_value(emergency.PerformedOnThisBattery, 0x0a) + + def test_emergency_read_all(self): + values = self.bus.run_sequence(emergency.BANK_208.read_all(0)) + self.assertEqual(values[emergency.RatedDuration], 180) + self.assertEqual(values[emergency.ControlGearTemperature], 30) + self.assertEqual(values[emergency.LampEmergencyTimeTotal], 1000) + self.assertEqual(values[emergency.StartFunctionTestCounter], 50) + def test_info(self): # Default bank 0 contents from fakes.py self._test_value(info.GTIN, 1234567654321)