Giter VIP home page Giter VIP logo

pmatic's People

Contributors

daris-embiq avatar jrester avatar jschmer avatar larsmichelsen avatar rolf-hempel avatar tschechniker avatar ybmpraktikant avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pmatic's Issues

list_room_states.py: pmatic.exceptions.PMException

When using the latest git master sources I get the following error message when trying the list_room_stats.py example script:

HWR
  Rauchmelder-Alarmaktor: State: off
  Fenster-HWR: closed
  Heizkörper-HWR: Temperature: 17.40 °C (Target: 15.00 °C, Valve: 0%)
  Rauchmelder-HWR: State: False
  Stromzufuhr-Ladegerät: State: off, Boot: True, Current: 0.00 mA, Energy Counter: 13132.70 Wh, Frequency: 49.99 Hz, Power: 0.00 W, Voltage: 233.40 V
Gästezimmer
  Fenster-GZ-Rechts: closed
Traceback (most recent call last):
  File "./list_room_states.py", line 28, in <module>
    print("  %s: %s" % (device.name, device.summary_state))
  File "build/bdist.linux-x86_64/egg/pmatic/entities.py", line 911, in summary_state
  File "build/bdist.linux-x86_64/egg/pmatic/entities.py", line 937, in _get_summary_state
  File "build/bdist.linux-x86_64/egg/pmatic/entities.py", line 310, in summary_state
  File "build/bdist.linux-x86_64/egg/pmatic/entities.py", line 216, in values
  File "build/bdist.linux-x86_64/egg/pmatic/entities.py", line 288, in _fetch_values
  File "build/bdist.linux-x86_64/egg/pmatic/api.py", line 178, in <lambda>
  File "build/bdist.linux-x86_64/egg/pmatic/api.py", line 493, in call
  File "build/bdist.linux-x86_64/egg/pmatic/api.py", line 150, in _parse_api_response
pmatic.exceptions.PMException: [interface_get_paramset] JSONRPCError: TCL error (601)

My CCU is a RaspberryMatic 2.15.5

Add necessary modules to use "setup.py install" on CCU

I was looking for something like this for some time now and I just found your project today and directly installed it on my CCU. I really like it.

What I miss right now is a way to install python modules on the CCU with "setup.py install". In my case I already use qhue.py (a slim Philips hue wrapper) from my PC and would like to use it from the CCU. I applied a quick fix by copying the qhue source files to the scripts directory on the CCU and can say that the CCU can run it. But unfortunately when I want to use the inline run option it does not work this way since the qhue files are not in the right directory (I assume so and have not figured out why there is no PYTHONPATH enviroment variable).

I don't think that it is possible right now to correctly install new modules right now since a lot of python modules for this task are missing.
I tried to fix it by installing setuptools by myself but there I had to apply a lot of fixes like copying distutils, shutils, zipfile, getopt, ConfigParser, plistlib (possibly more) to get it to work step by step. In the end I got stuck when I got this ImportError:

The 'packaging.requirements' package is required; normally this is bundled with this package so if you get this warning, consult the packager of your distribution.

Maybe this Error comes from my setuptools installer but I think it would be easier for everyone if the modules used by setup.py were directly delivered with the pmatic ccu package.

Add Homegear (XML-API) support

From having a quick glance at this repository I believe, that pmatic is not capable of serving as an interface to Homegear. Implementing this could lead to a much bigger audience, since there are quite a few users running Homegear on Raspberry Pis together with the HM-CFG-LAN or USB inteface instead of using the CCU.
Like the CCU, Homegear exposes a XML-RPC API on port 2001, but does not provide a web-based interface. Hence, everything has to be done through that API.

I have done that already as a separate package for another project, but from reading the documentation of pmatic I believe pmatic already has greater functionality and a more robust codebase. To me it would make more sense to integrate the XML-API usage into pmatic instead of further progressing with my current work.

If anyone is interested in doing this, my package is called pyhomematic. It's actually quite easy to use the API. I just don't have the time to look at how to get this into pmatic.

So, yeah, any thought on this? I suppose using the API could replace the existing remote-implementation, with the drawback, that the XML-API doesn't use any authentication.

pmatic-manager does not start

The pmatic-manager is not running. Starting it manually results in an ImportError:

# /usr/local/bin/python /usr/local/bin/pmatic-manager
Traceback (most recent call last):
  File "/usr/local/bin/pmatic-manager", line 66, in <module>
    import pmatic.manager
  File "/usr/local/etc/config/addons/pmatic/python/lib/python2.7/pmatic/manager.py", line 41, in <module>
    import inspect
ImportError: No module named inspect

Maximum recursion depth exceeded while calling a Python object

After some minutes of running the temperature update example on an Raspberrypi in conjunction with an CCU2 I get the following exception, which seems then repeated for all further updates:

2016-06-30 09:23:20,719 [ERROR] Exception in XML-RPC call event('pmatic-0', 'MEQ0799086:4', 'CONTROL_MODE', 0):
Traceback (most recent call last):
  File "build/bdist.linux-armv7l/egg/pmatic/events.py", line 349, in _dispatch
    return func(*params)
  File "build/bdist.linux-armv7l/egg/pmatic/events.py", line 378, in event
    param = obj.values[value_key]
  File "build/bdist.linux-armv7l/egg/pmatic/entities.py", line 219, in values
    self._fetch_values()
  File "build/bdist.linux-armv7l/egg/pmatic/entities.py", line 305, in _fetch_values
    self._values[param_id].set_from_api(value)
  File "build/bdist.linux-armv7l/egg/pmatic/params.py", line 184, in set_from_api
    return self._set_value(self._from_api_value(value))
  File "build/bdist.linux-armv7l/egg/pmatic/params.py", line 416, in _set_value
    return super(ParameterFLOAT, self)._set_value(float(value))
  File "build/bdist.linux-armv7l/egg/pmatic/params.py", line 203, in _set_value
    self._callback("value_updated")
  File "build/bdist.linux-armv7l/egg/pmatic/utils.py", line 111, in _callback
    raise PMException("Exception in callback (%s - %s): %s" % (cb_name, callback, e))
PMException: Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>): Exception in callback (value_updated - <function 
[ much more lines like this ]
Exception in callback (value_updated - <function print_summary_state at 0x76ac9bf0>):
[/]
 Unable to open "http://10.90.90.80/api/homematic.cgi" [RuntimeError]: maximum recursion depth exceeded while calling a Python object

Suggestion: Rename 'pmatic' to 'pymatic'

Even though this sounds like a minor unimportant thing, I would like to (in this early phase of this project) propose that 'pmatic' should be renamed to be actually called 'pymatic' to better point out that this project is about "python". In addition, pymatic IMHO sounds way better and can be more easily be pronounced than "pmatic" which seems a little be odd. In addition, it is quite common that python projects like this have a prefix starting with "pyXXXXX" to immediately point out that this is about python.

I know, a minor thing. But IMHO we should at least discuss this in this early phase of the project because now renaming it shouldn't be a big hassle. So please, lets see what the general opinions are about that ;)

Parameter-Parsing klappt nicht für Paramter mit Einheit % und Typ float

Das Gerät HM-TC-IT-WM-W-EU macht wieder Probleme:

Es hat einen Parameter 'HUMIDITY' in Kanal 1 mit folgender spec:
Parameter HUMIDITY
Typ: integer
Zugriffsart:
o lesend
o über Ereignisse
Minimaler Wert: 0
Maximaler Wert: 99
Einheit: %

aber auch diesen Parameter in Kanal 2:
Parameter ACTUAL_HUMIDITY
Typ: float
Zugriffsart:
o lesend
o über Ereignisse
Minimaler Wert: 0.0
Maximaler Wert: 99.0
Einheit: %

Aktuell wird für Parameter mit EInheit '%' immer die Klasse 'ParameterPERCENTAGE' ausgewählt. Bei der Umwandlung der Values via _transform_attributes tritt dann folgender ValueError auf:

Traceback (most recent call last):
File "C:\Program Files (x86)\JetBrains\PyCharm Community Edition 5.0.3\helpers\pydev\pydevd.py", line 2407, in
globals = debugger.run(setup['file'], None, None, is_module)
File "C:\Program Files (x86)\JetBrains\PyCharm Community Edition 5.0.3\helpers\pydev\pydevd.py", line 1798, in run
launch(file, globals, locals) # execute the script
File "C:/Projects/Py/pmatic/playground.py", line 48, in
print(" ", channel.name, channel.address, channel.summary_state)
File "C:/Projects/Py/pmatic\pmatic\entities.py", line 350, in summary_state
for title, value in sorted([ (v.name, v) for v in self.values.values() if v.readable ]):
File "C:/Projects/Py/pmatic\pmatic\entities.py", line 213, in values
self._init_value_specs()
File "C:/Projects/Py/pmatic\pmatic\entities.py", line 243, in _init_value_specs
self._values[param_id] = cls(self, param_spec)
File "C:/Projects/Py/pmatic\pmatic\params.py", line 60, in init
self._init_attributes(spec)
File "C:/Projects/Py/pmatic\pmatic\params.py", line 78, in _init_attributes
val = trans_func(val)
File "C:/Projects/Py/pmatic\pmatic\params.py", line 398, in
MIN = lambda v: int(v)+1,
ValueError: invalid literal for int() with base 10: '0.000000'

pmatic.exceptions.PMException: JSONRPCError

I am trying to register callbacks for device status updates. My test code is very simple, based on the callbacks sample code:

import pmatic
pmatic.logging(pmatic.DEBUG)
import time

def on_update(param):
	print("%s %s" % (param.channel.device.name, param.channel.summary_state))

if __name__=="__main__":
	ccu = pmatic.CCU ( address="http://ccu2.example.com", credentials=creds )

	devices = ccu.devices.query() 
	
	devices.on_value_updated(on_update)
		
	ccu.events.init()
	ccu.events.wait()
	ccu.events.close()

However, every single time I run this code, it fails with the following error:

  File "venv\lib\site-packages\pmatic\api.py", line 160, in _parse_api_response
    kwargs))
pmatic.exceptions.PMException: [interface_get_paramset_description] JSONRPCError: XML-RPC: unknown paramset (Code: 503, Request: {'interface': 'BidCos-RF', 'address': 'NEQ1596374:4', 'paramsetType': 'VALUES', '_session_id_': 'Qi0AbqX0fW'})

The device mentioned in the message is an HM-Dis-EP-WM55 (e-paper display).

Any help would be appreciated :)

Thank you!

smtplib missing on CCU

I'd like to send basic emails with smtplib that comes bundled with python.

This module does not exist on the current CCU installation. Is there another method to send emails that are already bundled on the CCU python installation?

getting a lot of errors with this library all the time

I'm trying this lib in python 3.9 on windows 10, the ccu3 has a firmware version 3.55.5, I didn't install anything extra on the ccu side
when I run any of the examples I always get something like: pmatic.exceptions.PMException: Method "room_get_all" is not a valid method.
for any method, the last one is the example of getting all the room names, am I doing something wrong? does this lib works in windows 10?

Support for Homematic IP

Continuing from the conversation that I started here, (but was in the wrong place).

Lars, following up on the guidance you gave I had a look. In api.py/DeviceSpecs you set the interface to BidCos-RF, which I suspect is only the classic HM.

for spec in self._api.interface_list_devices(interface="BidCos-RF"):

When you query for the interfaces you get this:

[interface['name'] for interface in self._api.interface_list_interfaces()]
=> ['BidCos-RF', 'VirtualDevices', 'HmIP-RF']

We'll get a little bit further when using HmIP-RF as well. However than I ran into another issue. The additional specs provided by HmIP-RF seem to collide with the model you had in mind.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mkamp/repos/code/pmatic/pmatic/ccu.py", line 288, in query
    for device in self._query_for_devices(**filters):
  File "/Users/mkamp/repos/code/pmatic/pmatic/ccu.py", line 305, in _query_for_devices
    device = self._create_from_low_level_dict(spec)
  File "/Users/mkamp/repos/code/pmatic/pmatic/ccu.py", line 358, in _create_from_low_level_dict
    device = Device.from_dict(self._ccu, spec)
  File "/Users/mkamp/repos/code/pmatic/pmatic/entities.py", line 955, in from_dict
    return device_class(ccu, spec)
  File "/Users/mkamp/repos/code/pmatic/pmatic/entities.py", line 944, in __init__
    super(Device, self).__init__(ccu, spec)
  File "/Users/mkamp/repos/code/pmatic/pmatic/entities.py", line 53, in __init__
    self._verify_mandatory_attributes()
  File "/Users/mkamp/repos/code/pmatic/pmatic/entities.py", line 101, in _verify_mandatory_attributes
    raise PMException("The mandatory attribute \"%s\" is missing." % key)
pmatic.exceptions.PMException: The mandatory attribute "channels" is missing.

I unfortunately don't really have the time to work on this in earnest and there is so much around (your code, the API, homematic, Python) that I need to learn about which make it hard to do something quickly.

If you don't mind I will continue slowly, no promises) and document what I learn along the way. For you or anybody else to pitch in, so that it eventually gets solved (or eventually closed).

Btw. Nice and well documented code.

JSONRPCError: missing argument (paramsetKey) Code: 402

Hello, I have a raspberry PI with pivCCU. When I try to use pmatic on the raspberry to communicate with the ccu, I always get an error, when I want to read device-specific values.
When I write this:

import pmatic
ccu = pmatic.CCU(address="10.0.0.16", credentials=("Admin", ""))
devices = ccu.devices.query(device_type=["HM-CC-RT-DN"])
for device in devices:
print(device.name)

then I get:

HM-CC-RT-DN OEQ1706899

That's good since I have connected exactly this thermostate with my CCU. So there is a communication with it, But when I try to read the temperature, like:
import pmatic
ccu = pmatic.CCU(address="10.0.0.16", credentials=("Admin", ""))
devices = ccu.devices.query(device_type=["HM-CC-RT-DN"])
for device in devices:
print(device.temperature)

then I always get this error:

Traceback (most recent call last):
File "pmatic_example.py", line 9, in
print(device.temperature)
File "/usr/local/lib/python2.7/dist-packages/pmatic/entities.py", line 1183, in temperature
return self.channels[4].values["ACTUAL_TEMPERATURE"]
File "/usr/local/lib/python2.7/dist-packages/pmatic/entities.py", line 216, in values
self._init_value_specs()
File "/usr/local/lib/python2.7/dist-packages/pmatic/entities.py", line 235, in _init_value_specs
address=self.address, paramsetType="VALUES"):
File "/usr/local/lib/python2.7/dist-packages/pmatic/api.py", line 190, in lowlevel_call
return self._call(method_name_int, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/pmatic/api.py", line 470, in _call
return self._do_call(method_name_int, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/pmatic/api.py", line 520, in _do_call
return self._parse_api_response(method_name_int, kwargs, response_txt)
File "/usr/local/lib/python2.7/dist-packages/pmatic/api.py", line 160, in _parse_api_response
kwargs))
pmatic.exceptions.PMException: [interface_get_paramset_description] JSONRPCError: missing argument (paramsetKey) (Code: 402, Request: {'interface': u'BidCos-RF', 'paramsetType': u'VALUES', u'session_id': u'prCQ39hNOl', 'address': u'OEQ1706899:4'})

I have less experience with python, so maybe it's just a small issue. Do I have to install any additional software? Anyone an idea?

BIN_DIR not created by Addon installer

After the addon installation on my CCU all files seem to be there but python was not in the PATH. It seems that the binary directory BIN_DIR=/usr/local/bin was missing so the setup/init scripts failed.
I fixed that by uninstall the addon, create the folder manually and install the addon again. Not it's working properly.

Python not installed on raspberrymatic

Hi,

I'm running Raspberrymatic ( 2.25.15.20161220) on an RPi 3.
The pmatic-manger could not be started from the WebUI, so i tested the installation of the addon by ssh-ing to the Raspberrymatic. When I try to run python from the commandline I only get an "-sh: python: not found". The same happens when i try "/usr/local/bin/python2.7". The Files are there but it says "not found".

pmatic initialization on CCU

According to the examples I tried this on the CCU2:

python
>>> import pmatic
>>> ccu = pmatic.CCU()

So no address and no credentials as I'm running it on the CCU locally. That fails with:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/etc/config/addons/pmatic/python/lib/python2.7/pmatic/ccu.py", line 67, in __init__
    self.api = pmatic.api.init(**kwargs)
  File "/usr/local/etc/config/addons/pmatic/python/lib/python2.7/pmatic/api.py", line 94, in init
    "to access your CCU (%s)." % e)
pmatic.exceptions.PMException: You need to provide at least the address and credentials to access your CCU (__init__() takes at least 3 arguments (1 given)).

Key Error 'device id '

#!/usr/bin/env python

encoding: utf-8

pmatic - Python API for Homematic. Easy to use.

Copyright (C) 2016 Lars Michelsen [email protected]

This program is free software; you can redistribute it and/or modify

it under the terms of the GNU General Public License as published by

the Free Software Foundation; either version 2 of the License, or

(at your option) any later version.

This program is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

GNU General Public License for more details.

You should have received a copy of the GNU General Public License along

with this program; if not, write to the Free Software Foundation, Inc.,

51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Relevant docs:

- http://www.eq-3.de/Downloads/PDFs/Dokumentation_und_Tutorials/HM_XmlRpc_V1_502__2_.pdf

- http://www.eq-3.de/Downloads/eq3/download%20bereich/hm_web_ui_doku/hm_devices_Endkunden.pdf

Add Python 3.x behaviour to 2.7

from future import absolute_import
from future import division
from future import print_function
from future import unicode_literals

import time
import threading

try:
from builtins import object # pylint:disable=redefined-builtin
except ImportError:
pass

import pmatic.params
import pmatic.utils as utils
from pmatic.exceptions import PMException, PMDeviceOffline

class Entity(object):
_transform_attributes = {}
_skip_attributes = []
_mandatory_attributes = []

def __init__(self, ccu, spec):
    assert isinstance(ccu, pmatic.ccu.CCU), "ccu is not of CCU class: %r" % ccu
    assert isinstance(spec, dict), "spec is not a dictionary: %r" % spec
    self._ccu = ccu
    self._set_attributes(spec)
    self._verify_mandatory_attributes()
    super(Entity, self).__init__()


def _set_attributes(self, obj_dict):
    """Adding provided attributes to this entity.

    Transforming and filtering dictionaries containing attributes for this entity
    by using the configured transform methods for the individual attributes and also
    excluding some attributes which keys are in self._skip_attributes."""
    for key, val in obj_dict.items():
        if key in self._skip_attributes:
            continue

        # Optionally convert values using the given transform functions
        # for the specific object type
        trans_func = self._transform_attributes.get(key)
        if trans_func:
            func_type = type(trans_func).__name__
            if func_type in [ "instancemethod", "function", "method" ]:
                args = []
                offset = 1 if func_type in [ "instancemethod", "method" ] else 0
                argcount = trans_func.__code__.co_argcount
                for arg_name in trans_func.__code__.co_varnames[offset:argcount]:
                    if arg_name == "api":
                        args.append(self._ccu.api)
                    elif arg_name == "ccu":
                        args.append(self._ccu)
                    elif arg_name == "device":
                        args.append(self)
                    elif arg_name == "obj":
                        args.append(self)
                    else:
                        args.append(val)
            else:
                args = [val]

            val = trans_func(*args)

        # Transform keys from camel case to our style
        key = utils.decamel(key)

        setattr(self, key, val)


def _verify_mandatory_attributes(self):
    for key in self._mandatory_attributes:
        if not hasattr(self, key):
            raise PMException("The mandatory attribute \"%s\" is missing." % key)

class Channels(dict):
"""This class has been created to make the dict where the channels are stored
in to have a similar interface like a list.

We want to have a consistent interface when e.g. doing this:

```
for device in ccu.devices:
    for channel in device.channels:
        ...
```

With only a dict for device.channels it would need device.channels.values()
"""
def __iter__(self):
    return iter(sorted(self.values(), key=lambda x: x.index))

class Channel(utils.LogMixin, Entity):
_transform_attributes = {
# ReGa attributes:
"id" : int,
"partner_id" : lambda x: None if x == "" else int(x),
# Low level attributes:
"aes_active" : bool,
"link_source_roles" : lambda v: v if isinstance(v, list) else v.split(" "),
"link_target_roles" : lambda v: v if isinstance(v, list) else v.split(" "),
}

# Don't add these keys to the objects attributes
_skip_attributes = [
    # Low level attributes:
    "parent",
    "parent_type",
]

# These keys have to be set after attribute initialization
_mandatory_attributes = [
    # Low level attributes:

    # address of channel
    "address",
    # communication direction of channel:
    # 0 = DIRECTION_NONE (Kanal unterstützt keine direkte Verknüpfung)
    # 1 = DIRECTION_SENDER
    # 2 = DIRECTION_RECEIVER
    "direction",
    # see device flags (0x01 visible, 0x02 internal, 0x08 can not be deleted)
    "flags",
    # channel number
    "index",
    # possible roles as sender
    "link_source_roles",
    # possible roles as receiver
    "link_target_roles",
    # list of available parameter sets
    "paramsets",
    # type of this channel
    "type",
    # version of the channel description
    "version",
]

def __init__(self, device, spec):
    if not isinstance(device, Device):
        raise PMException("Device object is not a Device derived class: %r" % device)
    self.device = device
    self._values = {}
    self._values_lock = threading.RLock()

    self._callbacks_to_register = {
        "value_updated": [],
        "value_changed": [],
    }

    super(Channel, self).__init__(device._ccu, spec)


@classmethod
def from_channel_dicts(cls, device, channel_dicts):
    """Creates channel instances associated with the given *device* instance from the given
    attribute dictionaries.

    Uses the list of channel attribute dictionaries given with *channel_dicts* to create a
    dictionary of specific `Channel` instances (like e.g. :class:`ChannelShutterContact`)
    or the generic :class:`Channel` class. Normally each channel should have a specific
    class. In case an unknown channel needs to be created a debug message is being logged.
    The dictionary uses the index of the channel (the channel id) as key for entries.

    The dictionary of the created channels is then returned."""
    channel_objects = Channels()
    for channel_dict in channel_dicts:
        channel_class = channel_classes_by_type_name.get(channel_dict["type"], Channel)

        if channel_class == Channel:
            cls.cls_logger().debug("Using generic Channel class (Type: %s): %r" %
                                                (channel_dict["type"], channel_dict))

        channel_objects[channel_dict["index"]] = channel_class(device, channel_dict)
    return channel_objects


@property
def values(self):
    """Provides access to all value objects of this channel.

    The values are provided as dictionary where the name of the parameter is used as key
    and some kind of specific :class:`.params.Parameter` instance is the value."""
    with self._values_lock:
        if not self._values:
            self._init_value_specs()

        if self._value_update_needed():
            self._fetch_values()

        return self._values


def _init_value_specs(self):
    """Initializes the value objects by fetching the specification from the CCU.

    The specification (description) of the VALUES paramset are fetched from
    the CCU and Parameter() objects will be created from them. Then they
    will be added to self._values.

    This method is called on the first access to the values.
    """
    self._values.clear()
    for value_spec in self._ccu.api.interface_get_paramset_description(interface="BidCos-RF",
                                                address=self.address, paramsetType="VALUES"):
        self._init_value_spec(value_spec)

    self._register_saved_callbacks()


def _init_value_spec(self, value_spec):
    """Initializes a single value of this channel."""
    value_id = value_spec["ID"]
    class_name = self._get_class_name_of_param_spec(value_spec)

    cls = getattr(pmatic.params, class_name)
    if not cls:
        self.logger.warning("%s: Skipping unknown parameter %s of type %s, unit %s. "
                            "Class %s not implemented." %
                    (self.channel_type, value_id, value_spec["TYPE"],
                     value_spec["UNIT"], class_name))
    else:
        self._values[value_id] = cls(self, value_spec)


def _get_class_name_of_param_spec(self, param_spec):
    """Gathers the name of the class to be used for creating a parameter object
    from the given parameter specification."""
    return "Parameter%s" % param_spec["TYPE"]


def _value_update_needed(self):
    """Tells whether or not the set of values should be fetched from the CCU."""
    oldest_value_time = None
    for param in self._values.values():
        try:
            last_updated = param.last_updated
            if last_updated == None:
                last_updated = 0 # enforce the update
        except PMException:
            continue # Ignore not readable values

        if oldest_value_time == None:
            oldest_value_time = last_updated
        elif last_updated < oldest_value_time:
            oldest_value_time = last_updated

    if oldest_value_time == None:
        return False # No readable value at all

    # FIXME: Make threshold configurable
    return oldest_value_time <= time.time() - 60


def _fetch_values(self):
    """Fetches all values of the channel.

    Gathers the values of the channel and updates the value parameters in self._values.
    The parameter objects need to be initialized before (self._init_value_specs).
    """
    if not self._values:
        raise PMException("The value parameters are not yet initialized.")

    try:
        values = self._get_values()
    except PMException as e:
        # FIXME: Clean this 601 in "%s" up!
        if "601" in ("%s" % e) and not self.device.is_online:
            raise PMDeviceOffline("Failed to fetch the values. The device is not online.")
        else:
            raise

    for param_id, value in values.items():
        if param_id in self._values:
            self._values[param_id].set_from_api(value)
        else:
            self.logger.info("%s (%s - %s): Skipping received value of unknown parameter %s.",
                                self.address, self.device.name, self.name, param_id)


def _get_values(self):
    """This method returns all values of this channel.

    Normally it is using the API call Interface.getParamset() to fetch all values of
    the channel at once. But there are devices which have bugged values which are reported
    to be readable, but can not be read in fact.

    The default way to deal with it is to catch the exectpion from the bulk get and then
    fetch the values one by one, again while catching the exceptions of these calls which
    which are raised when a value is not readable.

    In some cases where it is known to be an issue with specific values the channels in
    question have a specific class which overrides this method to fetch the single values
    one by one but skips the failing values explicitly."""
    try:
        return self._get_values_bulk()
    except PMException as e:
        # Can not check is_online for maintenance channels here (no values yet)
        if any(errorcode in ("%s" % e) for errorcode in ["501", "601"]) \
           and (isinstance(self, ChannelMaintenance) or self.device.is_online):
            self.logger.info("%s (%s - %s): %s. Falling back to single value fetching.",
                                self.address, self.device.name, self.name, e)
            return self._get_values_single(skip_invalid_values=True)
        else:
            raise


def _get_values_bulk(self):
    """Fetches all values of this channel at once. This is the default method to
    fetch the values."""
    return self._ccu.api.interface_get_paramset(interface="BidCos-RF",
                                     address=self.address, paramsetKey="VALUES")


def _get_values_single(self, skip_invalid_values=False):
    """Fetches all values known to be readable one by one. One should always
    use :meth:`_get_values_bulked` when possible. This is only used for buggy
    devices.

    The function can be called with the `skip_invalid_values` argument set to `True`
    to only log exceptions of single values and continue with the next value."""
    values = {}
    for param_id, value in self._values.items():
        if value.readable:
            try:
                values[value.id] = self._ccu.api.interface_get_value(
                                                    interface="BidCos-RF",
                                                    address=self.address,
                                                    valueKey=value.internal_name)
            except PMException as e:
                if not skip_invalid_values:
                    raise

                if not any(errorcode in ("%s" % e) for errorcode in ["501", "601"]):
                    raise

                if isinstance(self, ChannelMaintenance) or self.device.is_online:
                    self.logger.info("%s (%s - %s - %s): %s",
                            self.address, self.device.name, self.name, param_id, e)
                else:
                    raise
    return values


@property
def summary_state(self):
    """Represents a summary state of the channel.

    Formats values and titles of channel values and returns them as string.

    Default formating of channel values. Concatenates titles and values of
    all channel values except the maintenance channel.
    The values are sorted by the titles."""
    formated = []
    for title, value in sorted([ (v.name, v) for v in self.values.values() if v.readable ]):
        formated.append("%s: %s" % (title, value))
    return ", ".join(formated)


def set_logic_attributes(self, attrs):
    """Used to update the logic attributes of this channel.

    Applying the attributes in the dictionary to this object. Special handling
    for some attributes which are already set by the low level attributes."""
    #import pprint
    #pprint.pprint(self.__dict__)
    #pprint.pprint(attrs)
    #sys.exit(1)
    # Skip non needed attributes (already set by low level data)
    # FIXME: 'direction': 1, from low level API might be duplicate of
    #        u'category': u'CATEGORY_SENDER',
    # FIXME: 'aes_active': True, from low level API might be duplicate
    #        of u'mode': u'MODE_AES',
    attrs = attrs.copy()
    for a in [ "address", "device_id" ]:
   if a in attrs:

          del attrs[a]
    self._set_attributes(attrs)


def on_value_changed(self, func):
    """Register a function to be called each time a value of this channel parameters
    has changed."""
    try:
        values = self.values.values()
    except PMDeviceOffline:
        # Unable to register with parameters right now. Save for later registration
        # when values are available one day.
        self._save_callback_to_register("value_changed", func)
        return

    for value in values:
        value.register_callback("value_changed", func)


def on_value_updated(self, func):
    """Register a function to be called each time a value of this channel parameters has
    been updated."""
    try:
        values = self.values.values()
    except PMDeviceOffline:
        # Unable to register with parameters right now. Save for later registration
        # when values are available one day.
        self._save_callback_to_register("value_updated", func)
        return

    for value in values:
        value.register_callback("value_updated", func)


def _save_callback_to_register(self, cb_name, func):
    """Stores a callback function for attaching it later to the parameters."""
    self._callbacks_to_register[cb_name].append(func)


def _register_saved_callbacks(self):
    """If there are saved callbacks to register to new fetched parameters, register them!"""
    values = self._values.values()

    for cb_name, callbacks in self._callbacks_to_register.items():
        if not callbacks:
            continue

        for func in callbacks:
            for value in values:
                value.register_callback(cb_name, func)

FIXME: Implement this. The Device() object already has a lot of methods

related to the maintenance channel. Shouldn't they be moved here and evantually

only be available as shortcut in the Device object?

class ChannelMaintenance(Channel):
type_name = "MAINTENANCE"
name = "Maintenance"
id = 0

@property
def summary_state(self):
    """The maintenance channel does not provide a summary state.

    If you want to get a formated maintenance state, you need to use the property
    :attr:`maintenance_state`."""
    return None


@property
def maintenance_state(self):
    """Provides the formated maintenance state of the associated device."""
    return super(ChannelMaintenance, self).summary_state

FIXME: Handle LOWBAT/ERROR

class ChannelShutterContact(Channel):
type_name = "SHUTTER_CONTACT"

@property
def is_open(self):
    """``True`` when the contact is reported to be open, otherwise ``False``."""
    return self.values["STATE"].value


@property
def summary_state(self):
    """Provides a well formated state as string. It is ``open`` when the contact
    is open, otherwise ``closed``."""
    return self.is_open and "open" or "closed"

FIXME: Handle STOP, INHIBIT, INSTALL_TEST

class ChannelBlind(Channel):
type_name = "BLIND"

@property
def level(self):
    """Look up the level at which the shutter is set."""
    return self.values["LEVEL"].value

def set_level(self, level):
    """Set the level at which the shutter is to be set."""
    return self.values["LEVEL"].set(level)

@property
def working(self):
    """Look up the WORKING value."""
    return self.values["WORKING"].value

FIXME: Handle INHIBIT, WORKING

class ChannelSwitch(Channel):
type_name = "SWITCH"

@property
def is_on(self):
    """``True`` when the power is on, otherwise ``False``."""
    return self.values["STATE"].value


@property
def summary_state(self):
    """Provides the current state as well formated string."""
    return "%s: %s" % (self.values["STATE"].name, self.is_on and "on" or "off")


def toggle(self):
    """Use this to toggle the switch."""
    if self.is_on:
        return self.switch_off()
    else:
        return self.switch_on()


def switch_off(self):
    """Power off!"""
    return self.values["STATE"].set(False)


def switch_on(self):
    """Lights on!"""
    return self.values["STATE"].set(True)

FIXME: Handle LED_STATUS, ALL_LEDS, LED_SLEEP_MODE, INSTALL_TEST

class ChannelKey(Channel):
type_name = "KEY"

def press_short(self):
    """Call this to trigger a short press."""
    return self.values["PRESS_SHORT"].set(True)


def press_long(self):
    """Triggers a long press."""
    return self.values["PRESS_LONG"].set(True)


# Not verified working
def press_long_release(self):
    """Triggers the release of a long press."""
    return self.values["PRESS_LONG_RELEASE"].set(True)


# Not verified
def press_cont(self):
    """Unknown. Untested. Please let me know what this is."""
    return self.values["PRESS_CONT"].set(True)


@property
def summary_state(self):
    """Has no state info as it's a toggle button. This is only to override the
    default summary_state property."""
    return None

class ChannelVirtualKey(ChannelKey):
type_name = "VIRTUAL_KEY"

FIXME: Handle all values:

{u'POWER': u'3.520000', u'ENERGY_COUNTER': u'501.400000', u'BOOT': u'1',

u'CURRENT': u'26.000000', u'FREQUENCY': u'50.010000', u'VOLTAGE': u'228.900000'}

class ChannelPowermeter(Channel):
type_name = "POWERMETER"

FIXME: To be implemented.

class ChannelConditionPower(Channel):
type_name = "CONDITION_POWER"

FIXME: To be implemented.

class ChannelConditionCurrent(Channel):
type_name = "CONDITION_CURRENT"

FIXME: To be implemented.

class ChannelConditionVoltage(Channel):
type_name = "CONDITION_VOLTAGE"

FIXME: To be implemented.

class ChannelConditionFrequency(Channel):
type_name = "CONDITION_FREQUENCY"

FIXME: To be implemented.

Devices:

HM-Sen-LI-O

class ChannelLuxmeter(Channel):
type_name = "LUXMETER"

FIXME: To be implemented.

Devices:

HM-WDS10-TH-O

class ChannelWeather(Channel):
type_name = "WEATHER"

FIXME: Handle ERROR

class ChannelClimaVentDrive(Channel):
type_name = "CLIMATECONTROL_VENT_DRIVE"

FIXME: Handle ADJUSTING_COMMAND, ADJUSTING_DATA

class ChannelClimaRegulator(Channel):
type_name = "CLIMATECONTROL_REGULATOR"

@property
def summary_state(self):
    """Provides the ventil state."""
    val = self.values["SETPOINT"]
    if val == 0.0:
        return "Ventil closed"
    elif val == 100.0:
        return "Ventil open"
    else:
        return "Ventil: %s" % self.values["SETPOINT"]

Devices:

HM-CC-RT-DN

FIXME: Values:

{u'SET_TEMPERATURE': u'21.500000', u'PARTY_START_MONTH': u'1', u'BATTERY_STATE': u'2.400000',

u'PARTY_START_DAY': u'1', u'PARTY_STOP_DAY': u'1', u'PARTY_START_YEAR': u'0',

u'FAULT_REPORTING': u'0', u'PARTY_STOP_TIME': u'0', u'ACTUAL_TEMPERATURE': u'23.100000',

u'BOOST_STATE': u'15', u'PARTY_STOP_YEAR': u'0', u'PARTY_STOP_MONTH': u'1',

u'VALVE_STATE': u'10', u'PARTY_START_TIME': u'450', u'PARTY_TEMPERATURE': u'5.000000',

u'CONTROL_MODE': u'1'}

class ChannelClimaRTTransceiver(Channel):
type_name = "CLIMATECONTROL_RT_TRANSCEIVER"

@property
def summary_state(self):
    """Provides the actual and target temperature together with the valve state in
    some readable format."""
    return "Temperature: %s (Target: %s, Valve: %s)" % \
            (self.values["ACTUAL_TEMPERATURE"],
             self.values["SET_TEMPERATURE"],
             self.values["VALVE_STATE"])


def _get_class_name_of_param_spec(self, param_spec):
    if param_spec["ID"] == "CONTROL_MODE":
        return "ParameterControlMode"
    else:
        return super(ChannelClimaRTTransceiver, self)._get_class_name_of_param_spec(param_spec)

class ChannelWindowSwitchReceiver(Channel):
type_name = "WINDOW_SWITCH_RECEIVER"

@property
def summary_state(self):
    """Provides ``None`` since the channel has not any values"""
    return None

class ChannelWeatherReceiver(Channel):
type_name = "WEATHER_RECEIVER"

@property
def summary_state(self):
    """Provides ``None`` since the channel has not any values"""
    return None

Devices:

HM-CC-RT-DN

class ChannelClimateControlReceiver(Channel):
type_name = "CLIMATECONTROL_RECEIVER"

@property
def summary_state(self):
    """Provides ``None`` since the channel has not any values"""
    return None

Devices:

HM-CC-RT-DN

class ChannelClimateControlRTReceiver(Channel):
type_name = "CLIMATECONTROL_RT_RECEIVER"

@property
def summary_state(self):
    """Provides ``None`` since the channel has not any values"""
    return None

Devices:

HM-CC-RT-DN

class ChannelRemoteControlReceiver(Channel):
type_name = "REMOTECONTROL_RECEIVER"

@property
def summary_state(self):
    """Provides ``None`` since the channel has not any values"""
    return None

Devices:

HM-TC-IT-WM-W-EU

class ChannelWeatherTransmit(Channel):
type_name = "WEATHER_TRANSMIT"

@property
def summary_state(self):
    """Provides the temperature and humidity in readable format."""
    return "Temperature: %s, Humidity: %s" % \
            (self.values["TEMPERATURE"],
             self.values["HUMIDITY"])

Devices:

HM-TC-IT-WM-W-EU

class ChannelThermalControlTransmit(Channel):
type_name = "THERMALCONTROL_TRANSMIT"

def _init_value_spec(self, value_spec):
    # The value PARTY_MODE_SUBMIT seems to be declared to be readable by
    # the CCU which is wrong. This value can not be read.
    # See <https://github.com/LarsMichelsen/pmatic/issues/7>.
    if value_spec["ID"] == "PARTY_MODE_SUBMIT":
        value_spec["OPERATIONS"] = "2"
    super(ChannelThermalControlTransmit, self)._init_value_spec(value_spec)


def _get_values(self):
    # This is needed to not let the CCU decide which values to be read from
    # the device because of the bug mentioned above.
    return self._get_values_single()

Devices:

HM-TC-IT-WM-W-EU

class ChannelSwitchTransmit(Channel):
type_name = "SWITCH_TRANSMIT"

def _init_value_spec(self, value_spec):
    # The value SWITCH_TRANSMIT seems to be declared to be readable by
    # the CCU which is wrong. This value can not be read.
    # See <https://github.com/LarsMichelsen/pmatic/issues/7>.
    if value_spec["ID"] == "DECISION_VALUE":
        value_spec["OPERATIONS"] = "4" # only supports events
    super(ChannelSwitchTransmit, self)._init_value_spec(value_spec)


def _get_values(self):
    # This is needed to not let the CCU decide which values to be read from
    # the device because of the bug mentioned above.
    return self._get_values_single()

class Devices(object):
"""Manages a collection of CCU devices."""

def __init__(self, ccu):
    super(Devices, self).__init__()

    if not isinstance(ccu, pmatic.ccu.CCU):
        raise PMException("Invalid ccu object provided: %r" % ccu)
    self._ccu = ccu
    self._device_dict = {}


@property
def _devices(self):
    """Optional initializer of the devices data structure, called on first access."""
    return self._device_dict


def get(self, address, deflt=None):
    """Returns the device matching the given device address.

    If there is none matching the given address either None or the value
    specified by the optional attribute *deflt* is returned."""
    return self._devices.get(address, deflt)


def add(self, device):
    """Add a :class:`.Device` object to the collection."""
    if not isinstance(device, Device):
        raise PMException("You can only add device objects.")
    self._devices[device.address] = device


def exists(self, address):
    """Check whether or not a device with the given address is in this collection."""
    return address in self._devices


def addresses(self):
    """Returns a list of all addresses of all initialized devices."""
    return self._devices.keys()


def delete(self, address):
    """Deletes the device with the given address from the pmatic runtime.

    The device is not deleted from the CCU.
    When the device is not known, the method is tollerating that."""
    try:
        del self._devices[address]
    except KeyError:
        pass


def clear(self):
    """Remove all objects from this devices collection."""
    self._devices.clear()


def get_device_or_channel_by_address(self, address):
    """Returns the device or channel object of the given address.

    Raises a KeyError exception when no device exists for this
    address in the already fetched objects."""
    if ":" in address:
        device_address = address.split(":", 1)[0]
        return self._devices[device_address].channel_by_address(address)
    else:
        return self._devices[address]


def on_value_changed(self, func):
    """Register a function to be called each time a value of a device in this
    collection changed."""
    for device in self._devices.values():
        device.on_value_changed(func)


def on_value_updated(self, func):
    """Register a function to be called each time a value of a device in this
    collection updated."""
    for device in self._devices.values():
        device.on_value_updated(func)


def __iter__(self):
    """Provides an iterator over the devices of this collection."""
    for value in self._devices.values():
        yield value


def __len__(self):
    """Is e.g. used by :func:`len`. Returns the number of devices in this collection."""
    return len(self._devices)

FIXME: self.channels[0]: Provide better access to the channels. e.g. by names or ids or similar

class Device(Entity):
_transform_attributes = {
# ReGa attributes:
#"id" : int,
#"deviceId" : int,
#"operateGroupOnly" : lambda v: v != "false",

    # Low level attributes:
    "flags"             : int,
    "roaming"           : bool,
    "updateable"        : bool,
    "channels"          : Channel.from_channel_dicts,
}

# Don't add these keys to the objects attributes
_skip_attributes = [
    # Low level attributes:
    "children", # not needed
    "parent", # not needed
    "rf_address", # not available through XML-RPC and API, so exclude at all
    "rx_mode", # not available through XML-RPC and API, so exclude at all
]

# These keys have to be set after attribute initialization
_mandatory_attributes = [
    # Low level attributes:

    # Address of the device
    "address",
    # Firmware version string
    "firmware",
    # 0x01: show to user, 0x02 hide from user, 0x08 can not be deleted
    "flags",
    # serial number of the device
    "interface",
    # true when the device assignment is automatically adjusted
    "roaming",
    # device type
    "type",
    # true when an update is available
    "updatable",
    # version of the device description
    "version",
    # list of channel objects
    "channels",
]

def __init__(self, ccu, spec):
    super(Device, self).__init__(ccu, spec)


@classmethod
def from_dict(self, ccu, spec):
    """Creates a new device object from the attributes given in the *spec* dictionary.

    The *spec* dictionary needs to contain the mandatory attributes with values of the correct
    format. Depending on the device type specified by the *spec* dictionary, either a specific
    device class or the generic :class:`Device` class is used to create the object."""
    device_class = device_classes_by_type_name.get(spec["type"], Device)
    return device_class(ccu, spec)


# {u'UNREACH': u'1', u'AES_KEY': u'1', u'UPDATE_PENDING': u'1', u'RSSI_PEER': u'-65535',
#  u'LOWBAT': u'0', u'STICKY_UNREACH': u'1', u'DEVICE_IN_BOOTLOADER': u'0',
#  u'CONFIG_PENDING': u'0', u'RSSI_DEVICE': u'-65535', u'DUTYCYCLE': u'0'}
@property
def maintenance(self):
    """Returns the :class:`ChannelMaintenance` object of this device. It provides
    access to generic maintenance information available on this device."""
    return self.channels[0]


def set_logic_attributes(self, attrs):
    """Used to update the logic attributes of this device.

    Applying the attributes in the dictionary to this object. Special handling
    for some attributes which are already set by the low level attributes and
    for the channel attributes which are also part of attrs."""
    for channel_attrs in attrs["channels"]:
        self.channels[channel_attrs["index"]].set_logic_attributes(channel_attrs)

    # Skip non needed attributes (already set by low level data)
    attrs = attrs.copy()
    del attrs["channels"]
    del attrs["address"]
    del attrs["interface"]
    del attrs["type"]
    self._set_attributes(attrs)


@property
def is_online(self):
    """Is ``True`` when the device is currently reachable. Otherwise it is ``False``."""
    if self.type == "HM-RCV-50":
        return True # CCU is always assumed to be online
    else:
        return not self.maintenance.values["UNREACH"].value


@property
def is_battery_low(self):
    """Is ``True`` when the battery is reported to be low.

    When the battery is in normal state, it is ``False``. It might be a
    non battery powered device, then it is ``None``."""
    try:
        return self.maintenance.values["LOWBAT"].value
    except KeyError:
        return None # not battery powered


@property
def has_pending_config(self):
    """Is ``True`` when the CCU has pending configuration changes for this device.
    Otherwise it is ``False``."""
    if self.type == "HM-RCV-50":
        return False
    else:
        return self.maintenance.values["CONFIG_PENDING"].value


@property
def has_pending_update(self):
    """Is ``True`` when the CCU has a pending firmware update for this device.
    Otherwise it is ``False``."""
    try:
        return self.maintenance.values["UPDATE_PENDING"].value
    except KeyError:
        return False


@property
def rssi(self):
    """Is a two element tuple of the devices current RSSI (Received Signal Strength Indication).

    The first element is the devices RSSI, the second one the CCUs RSSI.

    In case of the CCU itself or a non radio device it is set to ``(None, None)``."""
    try:
        return self.maintenance.values["RSSI_DEVICE"].value, \
               self.maintenance.values["RSSI_PEER"].value
    except KeyError:
        return None, None


#{u'CONTROL': u'NONE', u'OPERATIONS': u'7', u'NAME': u'INHIBIT', u'MIN': u'0',
# u'DEFAULT': u'0', u'MAX': u'1', u'TAB_ORDER': u'6', u'FLAGS': u'1', u'TYPE': u'BOOL',
# u'ID': u'INHIBIT', u'UNIT': u''}
@property
def inhibit(self):
    """The actual inhibit state of the device.

    :getter: Whether or not the device is currently locked, provided as
             :class:`params.ParameterBOOL`.
    :setter: Specify the new inhibit state as boolean.
    :type: :class:`params.ParameterBOOL`/bool
    """
    return self.maintenance.values["INHIBIT"]


@inhibit.setter
def inhibit(self, state):
    self.maintenance.values["INHIBIT"].value = state


@property
def summary_state(self):
    """Provides a textual summary state of the device.

    Gives you a string representing some kind of summary state of the device. This
    string does not necessarly contain all state information of the devices.

    When a device is unreachable, it does only contain this information.

    This default method concatenates values and titles of channel values and
    provides them as string. The values are sorted by the titles."""
    return self._get_summary_state()


def _get_summary_state(self, skip_channel_types=None):
    """Internal helper for :prop:`summary_state`.

    It is possible to exclude the states of specific channels by listing the
    names of the channel classes in the optional *skip_channel_types* argument."""
    formated = []

    if not self.is_online:
        return "The device is unreachable"

    if self.is_battery_low:
        formated.append("The battery is low")

    if self.has_pending_config:
        formated.append("Config pending")

    if self.has_pending_update:
        formated.append("Update pending")

    # FIXME: Add bad rssi?

    for channel in self.channels:
        if skip_channel_types == None or type(channel).__name__ not in skip_channel_types:
            txt = channel.summary_state
            if txt != None:
                formated.append(txt)

    if formated:
        return ", ".join(formated)
    else:
        return "Device reports no operational state"


def channel_by_address(self, address):
    """Returns the channel object having the requested address.

    When the device has no such channel, a KeyError() is raised.
    """
    for channel in self.channels:
        if address == channel.address:
            return channel
    raise KeyError("The channel could not be found on this device.")


def on_value_changed(self, func):
    """Register a function to be called each time a value of this device has changed."""
    for channel in self.channels:
        channel.on_value_changed(func)


def on_value_updated(self, func):
    """Register a function to be called each time a value of this device has updated."""
    for channel in self.channels:
        channel.on_value_updated(func)

Funk-Heizkörperthermostat

TODO:

#{u'CONTROL': u'NONE', u'OPERATIONS': u'5', u'NAME': u'FAULT_REPORTING', u'MIN': u'0',

u'DEFAULT': u'0', u'MAX': u'7', u'VALUE_LIST': u'NO_FAULT VALVE_TIGHT

ADJUSTING_RANGE_TOO_LARGE ADJUSTING_RANGE_TOO_SMALL COMMUNICATION_ERROR {}

LOWBAT VALVE_ERROR_POSITION', u'TAB_ORDER': u'1', u'FLAGS': u'9',

u'TYPE': u'ENUM', u'ID': u'FAULT_REPORTING', u'UNIT': u''}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_TEMP', u'OPERATIONS': u'3', u'NAME': u'PARTY_TEMPERATURE',

u'MIN': u'5.000000', u'DEFAULT': u'20.000000', u'MAX': u'30.000000', u'TAB_ORDER': u'13',

u'FLAGS': u'1', u'TYPE': u'FLOAT', u'ID': u'PARTY_TEMPERATURE', u'UNIT': u'\xb0C'}

#{u'CONTROL': u'NONE', u'OPERATIONS': u'2', u'NAME': u'PARTY_MODE_SUBMIT', u'MIN': u'',

u'DEFAULT': u'', u'MAX': u'', u'TAB_ORDER': u'12', u'FLAGS': u'1', u'TYPE': u'STRING',

u'ID': u'PARTY_MODE_SUBMIT', u'UNIT': u''}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_TIME', u'OPERATIONS': u'3',

u'NAME': u'PARTY_START_TIME', u'MIN': u'0', u'DEFAULT': u'0', u'MAX': u'1410',

u'TAB_ORDER': u'14', u'FLAGS': u'1', u'TYPE': u'INTEGER', u'ID': u'PARTY_START_TIME',

u'UNIT': u'minutes'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_DAY', u'OPERATIONS': u'3', u'NAME': u'PARTY_START_DAY',

u'MIN': u'1', u'DEFAULT': u'1', u'MAX': u'31', u'TAB_ORDER': u'15', u'FLAGS': u'1',

u'TYPE': u'INTEGER', u'ID': u'PARTY_START_DAY', u'UNIT': u'day'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_MONTH', u'OPERATIONS': u'3',

u'NAME': u'PARTY_START_MONTH', u'MIN': u'1', u'DEFAULT': u'1', u'MAX': u'12',

u'TAB_ORDER': u'16', u'FLAGS': u'1', u'TYPE': u'INTEGER', u'ID':

u'PARTY_START_MONTH', u'UNIT': u'month'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_YEAR', u'OPERATIONS': u'3',

u'NAME': u'PARTY_START_YEAR', u'MIN': u'0', u'DEFAULT': u'12', u'MAX': u'99',

u'TAB_ORDER': u'17', u'FLAGS': u'1', u'TYPE': u'INTEGER', u'ID': u'PARTY_START_YEAR',

u'UNIT': u'year'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_TIME', u'OPERATIONS': u'3',

u'NAME': u'PARTY_STOP_TIME', u'MIN': u'0', u'DEFAULT': u'0', u'MAX': u'1410',

u'TAB_ORDER': u'18', u'FLAGS': u'1', u'TYPE': u'INTEGER', u'ID': u'PARTY_STOP_TIME',

u'UNIT': u'minutes'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_DAY', u'OPERATIONS': u'3', u'NAME': u'PARTY_STOP_DAY',

u'MIN': u'1', u'DEFAULT': u'1', u'MAX': u'31', u'TAB_ORDER': u'19', u'FLAGS': u'1',

u'TYPE': u'INTEGER', u'ID': u'PARTY_STOP_DAY', u'UNIT': u'day'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_MONTH', u'OPERATIONS': u'3', u'NAME': u'PARTY_STOP_MONTH',

u'MIN': u'1', u'DEFAULT': u'1', u'MAX': u'12', u'TAB_ORDER': u'20', u'FLAGS': u'1',

u'TYPE': u'INTEGER', u'ID': u'PARTY_STOP_MONTH', u'UNIT': u'month'}

#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_YEAR', u'OPERATIONS': u'3', u'NAME': u'PARTY_STOP_YEAR',

u'MIN': u'0', u'DEFAULT': u'12', u'MAX': u'99', u'TAB_ORDER': u'21', u'FLAGS': u'1',

u'TYPE': u'INTEGER', u'ID': u'PARTY_STOP_YEAR', u'UNIT': u'year'}

class HM_CC_RT_DN(Device):
type_name = "HM-CC-RT-DN"

@property
def temperature(self):
    """Provides the current temperature.

    Returns an instance of :class:`ParameterFLOAT`.
    """
    return self.channels[4].values["ACTUAL_TEMPERATURE"]


#{u'CONTROL': u'NONE', u'OPERATIONS': u'5', u'NAME': u'VALVE_STATE', u'MIN': u'0',
# u'DEFAULT': u'0', u'MAX': u'99', u'TAB_ORDER': u'3', u'FLAGS': u'1', u'TYPE': u'INTEGER',
# u'ID': u'VALVE_STATE', u'UNIT': u'%'}
@property
def valve_state(self):
    """Provides the current valve state in percentage.

    Returns an instance of :class:`ParameterINTEGER`.
    """
    return self.channels[4].values["VALVE_STATE"]


@property
def set_temperature(self):
    """The actual set temperature of the device.

    :getter: Provides the actual target temperature as :class:`ParameterFLOAT`.
    :setter: Specify the new set temperature as float. Please note that the CCU rounds
             this values to
             .0 or .5 after the comma. So if you provide .e.g 22.1 as new set temperature,
             the CCU will convert this to 22.0. This is totally equal to the control on the
             device.
    :type: ParameterFloat/float
    """
    return self.channels[4].values["SET_TEMPERATURE"]


@set_temperature.setter
def set_temperature(self, target):
    self.channels[4].values["SET_TEMPERATURE"].value = target


# {u'CONTROL': u'HEATING_CONTROL.COMFORT', u'OPERATIONS': u'2', u'NAME': u'COMFORT_MODE',
#  u'MIN': u'0', u'DEFAULT': u'0', u'MAX': u'1', u'TAB_ORDER': u'10', u'FLAGS': u'1',
#  u'TYPE': u'ACTION', u'ID': u'COMFORT_MODE', u'UNIT': u''}
def set_temperature_comfort(self):
    """Sets the :attr:`set_temperature` to the configured comfort temperature"""
    self.channels[4].values["COMFORT_MODE"].value = True


#{u'CONTROL': u'HEATING_CONTROL.LOWERING', u'OPERATIONS': u'2', u'NAME': u'LOWERING_MODE',
# u'MIN': u'0', u'DEFAULT': u'0', u'MAX': u'1', u'TAB_ORDER': u'11', u'FLAGS': u'1',
# u'TYPE': u'ACTION', u'ID': u'LOWERING_MODE', u'UNIT': u''}
def set_temperature_lowering(self):
    """Sets the :attr:`set_temperature` to the configured lowering temperature"""
    self.channels[4].values["LOWERING_MODE"].value = True


@property
def is_off(self):
    """Is set to `True` when the device is not enabled to heat."""
    return self.channels[4].values["SET_TEMPERATURE"].value == 4.5


def turn_off(self):
    """Call this method to tell the thermostat that it should not heat."""
    self.set_temperature = 4.5


@property
def control_mode(self):
    """
    The actual control mode of the device. This is either ``AUTO``, ``MANUAL``,
    ``PARTY`` or ``BOOST``.

    :getter: Provides the current control mode as :class:`ParameterENUM`.
    :setter: Set the control mode by the name of the mode (see above). When setting
             to ``MANUAL`` it uses either the current set temperature as target
             temperature or the default temperature when the
             device is currently turned off.
    :type: ParameterENUM/string
    """
    return self.channels[4].values["CONTROL_MODE"]


@control_mode.setter
def control_mode(self, mode):
    modes = ["AUTO", "MANUAL", "PARTY", "BOOST"]
    if mode not in modes:
        raise PMException("The control mode must be one of: %s" % ", ".join(modes))

    if mode == "MANUAL":
        mode = "MANU"

    value = True

    # In manual mode the set temperature needs to be provided. Set it to the
    # current set temperature. When the set temperature is "off", use the default
    # value.
    if mode == "MANU":
        if self.is_off:
            value = self.set_temperature.default
            # Also set the set_temperature attribute
            self.set_temperature = value
        else:
            value = self.set_temperature.value

    self.channels[4].values["%s_MODE" % mode].value = value


@property
def is_battery_low(self):
    """Is ``True`` when the battery is reported to be low, otherwise ``False``.
    If you want more details about the current battery, use :meth:`battery_state` to get
    the current reported voltage."""
    return self.channels[4].values["FAULT_REPORTING"].formated() == "LOWBAT"


@property
def battery_state(self):
    """Provides the actual battery voltage reported by the device."""
    return self.channels[4].values["BATTERY_STATE"]


# {u'CONTROL': u'NONE', u'OPERATIONS': u'5', u'NAME': u'BOOST_STATE', u'MIN': u'0',
#  u'DEFAULT': u'0', u'MAX': u'30', u'TAB_ORDER': u'4', u'FLAGS': u'1',
#  u'TYPE': u'INTEGER', u'ID': u'BOOST_STATE', u'UNIT': u'min'}
@property
def boost_duration(self):
    """When boost mode is currently active this returns the number of minutes left
    in boost mode. Otherwise it returns ``None``.

    Provides the configured boost duration as :class:`ParameterINTEGER`.
    """
    if self.control_mode == "BOOST":
        return self.channels[4].values["BOOST_STATE"]

Funk-Temperatur-/Luftfeuchtesensor OTH

class HM_WDS10_TH_O(Device):
type_name = "HM-WDS10-TH-O"

@property
def temperature(self):
    """Provides the current temperature.

    Returns an instance of :class:`ParameterFLOAT`.
    """
    return self.channels[1].values["TEMPERATURE"]


@property
def humidity(self):
    """Provides the current humidity.

    Returns an instance of :class:`ParameterFLOAT`.
    """
    return self.channels[1].values["HUMIDITY"]

Funk-Temperatur-/Luftfeuchtesensor ITH

class HM_WDS40_TH_I_2(Device):
type_name = "HM-WDS40-TH-I-2"

@property
def temperature(self):
    """Provides the current temperature.

    Returns an instance of :class:`ParameterFLOAT`.
    """
    return self.channels[1].values["TEMPERATURE"]


@property
def humidity(self):
    """Provides the current humidity.

    Returns an instance of :class:`ParameterFLOAT`.
    """
    return self.channels[1].values["HUMIDITY"]

Funk-Außen-Helligkeitssensor OLI

class HM_Sen_LI_O(Device):
type_name = "HM-Sen-LI-O"

@property
def brightness(self):
    """Provides the current brightness.

    Returns an instance of :class:`ParameterFLOAT`.
    """
    return self.channels[1].values["LUX"]

Virtuelle Fernbedienung der CCU

class HM_RCV_50(Device):
type_name = "HM-RCV-50"

Funk-Tür-/ Fensterkontakt

class HM_Sec_SC(Device):
type_name = "HM-Sec-SC"

# Make methods of ChannelShutterContact() available
def __getattr__(self, attr):
    return getattr(self.channels[1], attr)

Optischer Funk-Tür-/ Fensterkontakt

class HM_Sec_SCo(HM_Sec_SC):
type_name = "HM-Sec-SCo"

Funk-Schaltaktor mit Leistungsmessung

class HM_ES_PMSw1_Pl(Device):
type_name = "HM-ES-PMSw1-Pl"

# Make methods of ChannelSwitch() available
def __getattr__(self, attr):
    return getattr(self.channels[1], attr)


@property
def summary_state(self):
    return super(HM_ES_PMSw1_Pl, self)._get_summary_state(
        skip_channel_types=["ChannelConditionPower", "ChannelConditionCurrent",
                            "ChannelConditionVoltage", "ChannelConditionFrequency"])

Funk-Schaltaktor ohne Leistungsmessung

class HM_LC_Sw1_Pl_DN_R1(Device):
type_name = "HM-LC-Sw1-Pl-DN-R1"

# Make methods of ChannelSwitch() available
def __getattr__(self, attr):
    return getattr(self.channels[1], attr)


@property
def summary_state(self):
    return super(HM_LC_Sw1_Pl_DN_R1, self)._get_summary_state()

@property
def switch(self):
    """Provides to the :class:`.ChannelKey` object of the switch.

    You can do something like ``self.switch.switch_on()`` with this. For details take
    a look at the methods provided by the :class:``.ChannelKey`` class."""
    return self.channels[1]

Funk-Rolladenaktor

class HM_LC_Bl1PBU_FM(Device):
type_name = "HM-LC-Bl1PBU-FM"

# Make methods of ChannelBlind() available
def __getattr__(self, attr):
    return getattr(self.channels[1], attr)

@property
def blind(self):
    """Provides to the :class:`.ChannelKey` object of the blind channel.

    You can do something like ``self.blind.set_level(0.6)`` with this. For details take
    a look at the methods provided by the :class:``.ChannelKey`` class."""
    return self.channels[1]

class HM_PBI_4_FM(Device):
type_name = "HM-PBI-4-FM"

@property
def switch1(self):
    """Provides to the :class:`.ChannelKey` object of the first switch.

    You can do something like ``self.switch1.press_short()`` with this. For details take
    a look at the methods provided by the :class:``.ChannelKey`` class."""
    return self.channels[1]


@property
def switch2(self):
    """Provides to the :class:`.ChannelKey` object of the second switch."""
    return self.channels[2]


@property
def switch3(self):
    """Provides to the :class:`.ChannelKey` object of the third switch."""
    return self.channels[3]


@property
def switch4(self):
    """Provides to the :class:`.ChannelKey` object of the fourth switch."""
    return self.channels[4]

class Rooms(object):
"""Manages a collection of rooms."""

def __init__(self, ccu):
    super(Rooms, self).__init__()
    if not isinstance(ccu, pmatic.ccu.CCU):
        raise PMException("Invalid ccu object provided: %r" % ccu)
    self._ccu = ccu
    self._room_dict = {}


@property
def _rooms(self):
    """Optional initializer of the rooms data structure, called on first access."""
    return self._room_dict


def get(self, room_id, deflt=None):
    """Returns the :class:`Room` matching the given room id.

    If there is none matching the given ID either None or the value
    specified by the optional attribute *deflt* is returned."""
    return self._rooms.get(room_id, deflt)


@property
def ids(self):
    """Provides a sorted list of all ids of all initialized room."""
    return sorted(self._rooms.keys())


def add(self, room):
    """Add a :class:`Room` to the collection."""
    if not isinstance(room, Room):
        raise PMException("You can only add Room objects.")
    self._rooms[room.id] = room


def exists(self, room_id):
    """Check whether or not a :class:`Room` with the given id is in this collection."""
    return room_id in self._rooms


def delete(self, room_id):
    """Deletes the :class:`Room` with the given id from the pmatic runtime.

    The room is not deleted from the CCU. When the room is not known, the method is
    tollerating that."""
    try:
        del self._rooms[room_id]
    except KeyError:
        pass


def clear(self):
    """Remove all :class:`Room` objects from this collection."""
    self._rooms.clear()


def __iter__(self):
    """Provides an iterator over the rooms of this collection."""
    for value in self._rooms.values():
        yield value


def __len__(self):
    """Is e.g. used by :func:`len`. Returns the number of rooms in this collection."""
    return len(self._rooms)

class Room(Entity):
_transform_attributes = {
"id" : int,
"channelIds" : lambda x: list(map(int, x)),
}

def __init__(self, ccu, spec):
    self._values = {}
    self._devices = None
    super(Room, self).__init__(ccu, spec)


#@classmethod
#def get_rooms(self, api):
#    """Returns a list of all currently configured :class:`.Room` instances."""
#    rooms = []
#    for room_dict in api.room_get_all():
#        rooms.append(Room(api, room_dict))
#    return rooms


@property
def devices(self):
    """Provides access to a collection of :class:`.Device` objects which have at least one
    channel associated with this room.

    The collections is a :class:`.Devices` instance."""
    if not self._devices:
        self._devices = self._ccu.devices.query(has_channel_ids=self.channel_ids)
    return self._devices


@property
def channels(self):
    """Holds a list of channel objects associated with this room."""
    # FIXME: Cache this?
    room_channels = []
    for device in self.devices:
        for channel in device.channels:
            if channel.id in self.channel_ids:
                room_channels.append(channel)
    return room_channels

@Property

def programs(self):

"""Returns list of program objects which use at least one channel associated

with this room."""

# FIXME: Implement!

# FIXME: Cache this?

return []

def add(self, channel):

"""Adds a channel to this room."""

# FIXME: Implement!

def remove(self, channel):

"""Removes a channel to this room."""

# FIXME: Implement!

Build a list of all specific product classes. If a device is initialized

Device() checks whether or not a specific class or the generic Device()

class should be used to initialize an object.

device_classes_by_type_name = {}
for key, val in list(globals().items()):
if isinstance(val, type):
if issubclass(val, Device) and key != "Device":
device_classes_by_type_name[val.type_name] = val

channel_classes_by_type_name = {}
for key, val in list(globals().items()):
if isinstance(val, type):
if issubclass(val, Channel) and val != Channel:
channel_classes_by_type_name[val.type_name] = val

Manager does not react on pushbutton events

The manager doesn't react on "press short" or "press long" events. These events are also not shown in the event log tab of the pmatic manager. I tested with two devices: HM-PB-2-WM55 and the HM-RC-8 remote control.

Import error when requests module is not installed

Traceback (most recent call last):
  File "C:/Projects/Py/pmatic/debug_iterate_devices.py", line 31, in <module>
    import pmatic
  File "C:\Projects\Py\pmatic\pmatic\__init__.py", line 48, in <module>
    from pmatic.ccu import utils
  File "C:\Projects\Py\pmatic\pmatic\ccu.py", line 47, in <module>
    from pmatic.residents import Residents
  File "C:\Projects\Py\pmatic\pmatic\residents.py", line 29, in <module>
    import requests.exceptions
ImportError: No module named requests.exceptions

System variables

Is there a way to read system variables with pmatic? I am running pmatic on a raspberry pi.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.