larsmichelsen / pmatic Goto Github PK
View Code? Open in Web Editor NEWPython API for Homematic. Easy to use.
Home Page: https://larsmichelsen.github.io/pmatic/
License: GNU General Public License v2.0
Python API for Homematic. Easy to use.
Home Page: https://larsmichelsen.github.io/pmatic/
License: GNU General Public License v2.0
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?
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!
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 ;)
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?
The snapshot release https://larsmichelsen.github.io/pmatic/pmatic-snapshot_ccu.tar.gz from https://larsmichelsen.github.io/pmatic/ is not based on the last commit as your text suggests:
Snapshot releases are updated on each commit to the repository, but are not officially released versions. So they might not be as stable as released versions.
For example the snapshot does not include the ssl python module added with commit 00f03cb 4 days ago.
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'
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.
Line 828 in adc8fa7
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.
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)).
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
Is there a way to read system variables with pmatic? I am running pmatic on a raspberry pi.
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.
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".
Is there a brief explanation how to install Python on Raspberrymatic?
Thx Axel.
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.
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?
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
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
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.
#!/usr/bin/env python
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)
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
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"
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
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)
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"
class ChannelPowermeter(Channel):
type_name = "POWERMETER"
class ChannelConditionPower(Channel):
type_name = "CONDITION_POWER"
class ChannelConditionCurrent(Channel):
type_name = "CONDITION_CURRENT"
class ChannelConditionVoltage(Channel):
type_name = "CONDITION_VOLTAGE"
class ChannelConditionFrequency(Channel):
type_name = "CONDITION_FREQUENCY"
class ChannelLuxmeter(Channel):
type_name = "LUXMETER"
class ChannelWeather(Channel):
type_name = "WEATHER"
class ChannelClimaVentDrive(Channel):
type_name = "CLIMATECONTROL_VENT_DRIVE"
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"]
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
class ChannelClimateControlReceiver(Channel):
type_name = "CLIMATECONTROL_RECEIVER"
@property
def summary_state(self):
"""Provides ``None`` since the channel has not any values"""
return None
class ChannelClimateControlRTReceiver(Channel):
type_name = "CLIMATECONTROL_RT_RECEIVER"
@property
def summary_state(self):
"""Provides ``None`` since the channel has not any values"""
return None
class ChannelRemoteControlReceiver(Channel):
type_name = "REMOTECONTROL_RECEIVER"
@property
def summary_state(self):
"""Provides ``None`` since the channel has not any values"""
return None
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"])
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()
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)
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)
#{u'CONTROL': u'NONE', u'OPERATIONS': u'5', u'NAME': u'FAULT_REPORTING', u'MIN': u'0',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_TEMP', u'OPERATIONS': u'3', u'NAME': u'PARTY_TEMPERATURE',
#{u'CONTROL': u'NONE', u'OPERATIONS': u'2', u'NAME': u'PARTY_MODE_SUBMIT', u'MIN': u'',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_TIME', u'OPERATIONS': u'3',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_DAY', u'OPERATIONS': u'3', u'NAME': u'PARTY_START_DAY',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_MONTH', u'OPERATIONS': u'3',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_START_YEAR', u'OPERATIONS': u'3',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_TIME', u'OPERATIONS': u'3',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_DAY', u'OPERATIONS': u'3', u'NAME': u'PARTY_STOP_DAY',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_MONTH', u'OPERATIONS': u'3', u'NAME': u'PARTY_STOP_MONTH',
#{u'CONTROL': u'HEATING_CONTROL.PARTY_STOP_YEAR', u'OPERATIONS': u'3', u'NAME': u'PARTY_STOP_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"]
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"]
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"]
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"]
class HM_RCV_50(Device):
type_name = "HM-RCV-50"
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)
class HM_Sec_SCo(HM_Sec_SC):
type_name = "HM-Sec-SCo"
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"])
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]
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
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
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.
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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.