diff --git a/changelog.txt b/changelog.txt index 983a2c085..a11a7dea2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +Version 0.1.49 (2018-09-16) +- Rework RSSI handling (Issue #164, Issue #121) @klada +- Prevent failure of everything when single host is down (Issue #162) + Version 0.1.48 (2018-09-13) - Added Support for HmIP-SWO-PL and HmIP-SWO-B @dickesW - Fix callbacks for channel 0 @klada diff --git a/pyhomematic/_hm.py b/pyhomematic/_hm.py index 38acbed97..51cf838b5 100644 --- a/pyhomematic/_hm.py +++ b/pyhomematic/_hm.py @@ -462,6 +462,7 @@ def __init__(self, self.systemcallback = systemcallback self.resolveparamsets = resolveparamsets self.proxies = {} + self.failed_inits = [] # Create proxies to interact with CCU / Homegear LOG.debug("__init__: Creating proxies") @@ -551,12 +552,15 @@ def proxyInit(self): except Exception as err: LOG.debug("proxyInit: Exception: %s" % str(err)) LOG.warning("Failed to initialize proxy") - raise Exception + self.failed_inits.append(interface_id) def stop(self): """To stop the server we de-init from the CCU / Homegear, then shut down our XML-RPC server.""" stopped = [] - for _, proxy in self.proxies.items(): + for interface_id, proxy in self.proxies.items(): + if interface_id in self.failed_inits: + LOG.warning("ServerThread.stop: Not performing de-init for %s" % interface_id) + continue if proxy._callbackip and proxy._callbackport: callbackip = proxy._callbackip callbackport = proxy._callbackport @@ -573,7 +577,6 @@ def stop(self): except Exception as err: LOG.debug("proxyInit: Exception: %s" % str(err)) LOG.warning("Failed to de-initialize proxy") - raise Exception self.proxies.clear() LOG.info("Shutting down server") self.server.shutdown() diff --git a/pyhomematic/devicetypes/actors.py b/pyhomematic/devicetypes/actors.py index 4aa350aa3..21f66d108 100644 --- a/pyhomematic/devicetypes/actors.py +++ b/pyhomematic/devicetypes/actors.py @@ -4,7 +4,7 @@ from pyhomematic.devicetypes.misc import HMEvent from pyhomematic.devicetypes.helper import ( HelperWorking, HelperActorState, HelperActorLevel, HelperActorBlindTilt, HelperActionOnTime, - HelperActionPress, HelperEventRemote, HelperWired) + HelperActionPress, HelperEventRemote, HelperWired, HelperRssiPeer, HelperRssiDevice) LOG = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def stop(self, channel=None): self.actionNodeData("STOP", True, channel) -class Blind(GenericBlind, HelperWorking): +class Blind(GenericBlind, HelperWorking, HelperRssiPeer): """ Blind switch that raises and lowers roller shutters or window blinds. """ @@ -174,7 +174,7 @@ def ELEMENT(self): return [2] -class Switch(GenericSwitch, HelperWorking): +class Switch(GenericSwitch, HelperWorking, HelperRssiPeer): """ Switch turning plugged in device on or off. """ @@ -268,7 +268,7 @@ def ELEMENT(self): return self._doc -class RFSiren(GenericSwitch, HelperWorking): +class RFSiren(GenericSwitch, HelperWorking, HelperRssiPeer): """ HM-Sec-Sir-WM Siren """ @@ -286,7 +286,7 @@ def ELEMENT(self): return [1, 2, 3] -class KeyMatic(HMActor, HelperActorState): +class KeyMatic(HMActor, HelperActorState, HelperRssiPeer): """ Lock, Unlock or Open KeyMatic. """ @@ -377,7 +377,7 @@ def ELEMENT(self): return [1, 2] -class IPSwitchPowermeter(IPSwitch, HMSensor): +class IPSwitchPowermeter(IPSwitch, HMSensor, HelperRssiDevice): """ Switch turning plugged in device on or off and measuring energy consumption. """ diff --git a/pyhomematic/devicetypes/generic.py b/pyhomematic/devicetypes/generic.py index ea456433c..1157aa500 100644 --- a/pyhomematic/devicetypes/generic.py +++ b/pyhomematic/devicetypes/generic.py @@ -213,7 +213,7 @@ def __init__(self, device_description, proxy, resolveparamsets=False): # - 0...n / getValue from channel (fix) self._SENSORNODE = {} self._BINARYNODE = {} - self._ATTRIBUTENODE = {"RSSI_PEER": [0]} + self._ATTRIBUTENODE = {} self._WRITENODE = {} self._EVENTNODE = {} self._ACTIONNODE = {} @@ -334,7 +334,13 @@ def _setNodeData(self, name, metadata, data, channel=None): return False def get_rssi(self, channel=0): - return self.getAttributeData("RSSI_PEER", channel) + """ + This is a stub method which is implemented by the helpers + HelperRssiPeer/HelperRssiDevice in order to provide a suitable + implementation for the device. + """ + #pylint: disable=unused-argument + return 0 @property def ELEMENT(self): diff --git a/pyhomematic/devicetypes/helper.py b/pyhomematic/devicetypes/helper.py index 3f17f7c6d..10734fdd8 100644 --- a/pyhomematic/devicetypes/helper.py +++ b/pyhomematic/devicetypes/helper.py @@ -238,8 +238,28 @@ def __init__(self, device_description, proxy, resolveparamsets=False): "PRESS_LONG_RELEASE": self.ELEMENT}) class HelperWired(HMDevice): - """Remove the RSSI_PEER attribute""" + """Remove the RSSI-related attributes""" def __init__(self, device_description, proxy, resolveparamsets=False): super().__init__(device_description, proxy, resolveparamsets) - self.ATTRIBUTENODE.pop("RSSI_PEER", None) + self.ATTRIBUTENODE.pop("RSSI_DEVICE", None) + + +class HelperRssiDevice(HMDevice): + """Used for devices which report their RSSI value through RSSI_DEVICE""" + def __init__(self, device_description, proxy, resolveparamsets=False): + super().__init__(device_description, proxy, resolveparamsets) + self.ATTRIBUTENODE["RSSI_DEVICE"] = [0] + + def get_rssi(self, channel=0): + return self.getAttributeData("RSSI_DEVICE", channel) + + +class HelperRssiPeer(HMDevice): + """Used for devices which report their RSSI value through RSSI_PEER""" + def __init__(self, device_description, proxy, resolveparamsets=False): + super().__init__(device_description, proxy, resolveparamsets) + self.ATTRIBUTENODE["RSSI_PEER"] = [0] + + def get_rssi(self, channel=0): + return self.getAttributeData("RSSI_PEER", channel) diff --git a/pyhomematic/devicetypes/misc.py b/pyhomematic/devicetypes/misc.py index d2c4970ae..c93021a8f 100644 --- a/pyhomematic/devicetypes/misc.py +++ b/pyhomematic/devicetypes/misc.py @@ -1,6 +1,6 @@ import logging from pyhomematic.devicetypes.generic import HMDevice -from pyhomematic.devicetypes.helper import HelperActionPress, HelperEventRemote, HelperEventPress +from pyhomematic.devicetypes.helper import HelperActionPress, HelperEventRemote, HelperEventPress, HelperRssiPeer LOG = logging.getLogger(__name__) @@ -21,7 +21,7 @@ def ELEMENT(self): return [c for c in range(1, 51)] -class Remote(HMEvent, HelperEventRemote, HelperActionPress): +class Remote(HMEvent, HelperEventRemote, HelperActionPress, HelperRssiPeer): """Remote handle buttons.""" @property diff --git a/pyhomematic/devicetypes/sensors.py b/pyhomematic/devicetypes/sensors.py index 81d3c4ea6..2c9bb3a36 100644 --- a/pyhomematic/devicetypes/sensors.py +++ b/pyhomematic/devicetypes/sensors.py @@ -5,7 +5,7 @@ HelperLowBatIP, HelperSabotageIP, HelperBinaryState, HelperSensorState, - HelperWired, HelperEventRemote) + HelperWired, HelperEventRemote, HelperRssiPeer, HelperRssiDevice) LOG = logging.getLogger(__name__) @@ -35,7 +35,7 @@ def ELEMENT(self): return [1] -class ShutterContact(IPShutterContact, HelperSabotage): +class ShutterContact(IPShutterContact, HelperSabotage, HelperRssiPeer): """Door / Window contact that emits its open/closed state.""" pass @@ -60,7 +60,7 @@ def is_not_tilted(self, channel=None): return not self.get_state(channel) -class RotaryHandleSensor(HMSensor, HelperSensorState, HelperLowBat, HelperSabotage): +class RotaryHandleSensor(HMSensor, HelperSensorState, HelperLowBat, HelperSabotage, HelperRssiPeer): """Window handle contact.""" def is_open(self, channel=None): """ Returns True if the handle is set to open. """ @@ -105,7 +105,7 @@ def is_added_strong(self, channel=None): return self.get_state(channel) == 2 -class WaterSensor(HMSensor, HelperSensorState, HelperLowBat): +class WaterSensor(HMSensor, HelperSensorState, HelperLowBat, HelperRssiPeer): """Watter detect sensor.""" def is_dry(self, channel=None): @@ -121,7 +121,7 @@ def is_water(self, channel=None): return self.get_state(channel) == 2 -class PowermeterGas(HMSensor): +class PowermeterGas(HMSensor, HelperRssiPeer): """Powermeter for Gas and energy.""" def __init__(self, device_description, proxy, resolveparamsets=False): @@ -167,7 +167,7 @@ def __init__(self, device_description, proxy, resolveparamsets=False): "ERROR_SMOKE_CHAMBER": self.ELEMENT}) -class IPSmoke(HMSensor): +class IPSmoke(HMSensor, HelperRssiDevice): """HomeMatic IP Smoke sensor""" def __init__(self, device_description, proxy, resolveparamsets=False): diff --git a/pyhomematic/devicetypes/thermostats.py b/pyhomematic/devicetypes/thermostats.py index 434a37047..1a3d811dc 100644 --- a/pyhomematic/devicetypes/thermostats.py +++ b/pyhomematic/devicetypes/thermostats.py @@ -1,7 +1,7 @@ import logging from pyhomematic.devicetypes.generic import HMDevice from pyhomematic.devicetypes.sensors import AreaThermostat -from pyhomematic.devicetypes.helper import HelperValveState, HelperBatteryState, HelperLowBat, HelperLowBatIP +from pyhomematic.devicetypes.helper import HelperValveState, HelperBatteryState, HelperLowBat, HelperLowBatIP, HelperRssiPeer LOG = logging.getLogger(__name__) @@ -123,7 +123,7 @@ def __init__(self, device_description, proxy, resolveparamsets=False): "CONTROL_MODE": [1]}) -class Thermostat(HMThermostat, HelperBatteryState, HelperValveState): +class Thermostat(HMThermostat, HelperBatteryState, HelperValveState, HelperRssiPeer): """ HM-CC-RT-DN, HM-CC-RT-DN-BoM ClimateControl-Radiator Thermostat that measures temperature and allows to set a target temperature or use some automatic mode. @@ -144,7 +144,7 @@ def __init__(self, device_description, proxy, resolveparamsets=False): "CONTROL_MODE": [4]}) -class ThermostatWall(HMThermostat, AreaThermostat, HelperBatteryState): +class ThermostatWall(HMThermostat, AreaThermostat, HelperBatteryState, HelperRssiPeer): """ HM-TC-IT-WM-W-EU ClimateControl-Wall Thermostat that measures temperature and allows to set a target temperature or use some automatic mode. diff --git a/setup.py b/setup.py index 612667dfb..13d0bf345 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def readme(): PACKAGE_NAME = 'pyhomematic' HERE = os.path.abspath(os.path.dirname(__file__)) -VERSION = '0.1.48' +VERSION = '0.1.49' PACKAGES = find_packages(exclude=['tests', 'tests.*', 'dist', 'build']) diff --git a/tests/test_pyhomematic.py b/tests/test_pyhomematic.py index 3419c289c..6b960944c 100644 --- a/tests/test_pyhomematic.py +++ b/tests/test_pyhomematic.py @@ -8,6 +8,7 @@ from pyhomematic import vccu from pyhomematic import HMConnection from pyhomematic import devicetypes +from pyhomematic.devicetypes.helper import HelperRssiDevice, HelperRssiPeer logging.basicConfig(level=logging.INFO) LOG = logging.getLogger(__name__) @@ -145,5 +146,27 @@ def test_0_pyhomematic_classes(self): else: LOG.warning("Device class missing for: %s" % deviceobject.TYPE) + +class Test_3_HelperClassHierarchy(unittest.TestCase): + """ + Some helper classes are mutally exclusive (such as HelperRssiPeer/HelperRssiDevice). + + Since many device implementations inherit from other implementation classes we need + to be extra careful when plugging in such mutally excluse helpers somewhere in the + class hierarchy. This test case may be used for checking the currently available device + classes for such situations. + """ + def setUp(self): + self.device_classes = set(devicetypes.SUPPORTED.values()) + + def test_rssi_helper(self): + for klass in self.device_classes: + both_rssi_helpers_used = issubclass(HelperRssiDevice, klass) and issubclass(HelperRssiPeer, klass) + self.assertFalse( + both_rssi_helpers_used, + "The class %s inherits from both HelperRssiDevice and HelperRssiPeer, which is not supported." % klass + ) + + if __name__ == '__main__': unittest.main()