HABApp - Easy automation with openHAB

If it is not a special type (e.g. shutter, switch) the value gets converted to the corresponding python type:

if isinstance(item.state, str):
    # is a StringItem
elif isinstance(item.state, (float, int))
    # is a NumberItem
elif isinstance(item.state, datetime.datetime):
    # is a dateitem

However I’d do it the following:

def return_timestamp(item, format):
    dt = datetime.datetime.now()
    if item is not None:
        self.oh.postUpdate(item, dt)
    log.info( f'LastUpdate {item.name} ({type(item.state)}) at {dt}')

Another hint:
Since you can get items by name it’s easy to use naming conventions:

Number MyItem
DateTime MyItemLastChange
def update_item(item):
    self.oh.postUpdate(item, 0)
    self.oh.postUpdate(item.name + 'LastChange', datetime.datetime.now())

Version 0.8.0


Reworked Parameters:

  • can now be used to setup rules dynamically
  • can be imported and created through HABApp.Parameter
  • own documentation page

Reworked MultiValueItem:

  • Removed MultiValue
  • Added lots of documentation

Other:

  • run_in supports timedelta
  • Fixed example (fixes #72)
  • Added more documentation

Version 0.9.0

This is a rather huge release. Use the listen_only configuration switch to test your rules from another machine before updating!


Changes

  • Renamed functions and variables for item (added deprecation warning for old item.state property so it still works)
    • item.state -> item.value
    • item.set_state -> item.set_value
    • item.post_state -> item.post_value
    • item.get_state -> item.get_value
  • Moved configuration to EasyCo
  • MQTT configuration has listen_only switch, too
  • Added NumberItem to openhab.items
  • Updated lots documentation
  • Added functions with corresponding commands for OpenhabItems
  • Added command processing for OpenhabItems
  • Prepared for Openhab 2.5
  • Use relative names in testing configuration
  • Counter is thread safe
  • renamed ColorItem.value to ColorItem.brightness
  • MultiModeValue has function calculate_lower_priority_value which returns the low prio value

Version 0.9.1


  • Datetime bugfixes for negative UTC offsets and openHAB events
  • Fixed MQTT topic validator which prevented connections to a mqtt broker
  • Parameter accepts None as default_value which skips creation of the default key

Version 0.10.0

  • Configured logfiles get rotated on startup instead of deleted
  • added parameter “wait_for_openhab”. If true HABApp will wait until items
    from openhab have been loaded before loading the rules
  • Param & Rule file loading is now event based:
    Reloading of both file types can be observed and triggered from within rules
  • Deprecated some functions and added deprecation warnings to the log
  • Error Processing is now event based, too. Removed WrappedFunction._ERROR_CALLBACK.
    Added an example how to process rule error messages in documentation
  • Prepared parameter file validation
  • Added some more documentation
1 Like

Version 0.10.1


  • Serialized rule (un-) loading - fixes rule duplicates in rare cases
  • Added documentation for set_file_validator
  • added func get_path for FileEvents
  • Gracefully shutdown aiohttp on shutdown of HABApp
  • Openhab Tests log to own file
  • MyPy fixes

I have been working away on Habapp, and I’ve made some routines to emulate some OH2 functionality. Specifically

  • sendMail
  • pushNotification
  • say
  • CreateTimer

The say implementation is a bit specific, it sends text to one (or more) chromecast device(s), using google TTS service, and assumes you have a web server you can write the TTS files to, to serve the chromecast devices. You could serve them from a built in web server, but I have one on my OH2 server anyway, OH2 does as well.

Here is what I have:
SendMail (only tested with gmail)

import smtplib
from email.mime.multipart import MIMEMultipart 
from email.mime.text import MIMEText 
from email.mime.base import MIMEBase 
from email.mime.image import MIMEImage
from email.mime.application import MIMEApplication
from email import encoders

mail_from = 'me@gmail.com'
mail_to = ['someone@gmail.com']

class MailServer(HABApp.Rule):
    def __init__(self, server, port, login, password):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)

        self.__runtime = self._Rule__runtime
        
        self.serveraddr = server
        self.server_port = port
        self.login = login
        self.password = password
        
        self.loop = self.__runtime.loop
        self.q = asyncio.Queue(loop=self.loop)
        
        self.run_soon(self.Push)
        
    def send(self, subject, message, mail_from, mail_to, attachment):
        self.run_soon(self.add_to_queue, subject, message, mail_from, mail_to, attachment)
        
    async def add_to_queue(self, subject, message, mail_from, mail_to, attachment):
        await self.q.put((subject, message, mail_from, mail_to, attachment))

    async def Push(self):
        while True:
            subject, message, mail_from, mail_to, attachment = await self.q.get()
            await self.sendmail(subject, message, mail_from, mail_to, attachment)
            
    async def sendmail(self, subject='No Subject',  message = '', mail_from=None, mail_to=[], attachment=None):
        if not mail_to or (not message and not attachment):
            return
        try:
            msg = MIMEMultipart()
            msg['Subject'] = subject
            msg['From'] = mail_from
            msg['To'] = ",".join(mail_to)
            msg.attach(MIMEText(message))
            msg = await self.attach(msg, attachment)
            self.send_message(msg)
        except Exception as e:
            self.log.exception(e)
            
    async def attach(self, msg, attachment):
        if attachment:
            image = None
            if os.path.exists(attachment):
                resp = open(attachment, "rb") 
                image = resp.read()
            else:
                if attachment.startswith('http'):
                    async with self.async_http.get(attachment) as resp:
                        if resp.status == 200:
                            image = await resp.read()
            
            if image:
                p = MIMEImage(image, Name=os.path.basename(attachment))                       
                p.add_header('Content-Disposition', "attachment; filename= %s" % os.path.basename(attachment))  
                msg.attach(p)
        return msg
        
    def send_message(self, msg):
        with smtplib.SMTP( self.serveraddr, self.server_port ) as server:
            server.starttls()
            server.login( self.login, self.password )
            self.log.info('Mail message: %s sent' % msg['subject'])
            server.send_message( msg )
            
ms = MailServer('smtp.gmail.com', 587, 'your-email-here@gmail.com', 'your-paswword')

def sendMail(subject = 'No Subject', message='', attachment=None):
    ms.send(subject, message, mail_from, mail_to, attachment)

pushNotification (needs pushno library, and a prowl api key)

from pushno import PushNotification

class prowl(HABApp.Rule):
    def __init__(self, apikey=None):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)

        self.__runtime = self._Rule__runtime

        self.apikey = apikey
        if self.apikey is None:
            self.apikey = params.prowl_api.value
        self.pn = PushNotification("prowl", api_key=self.apikey, application="HABApp")
        
        self.loop = self.__runtime.loop
        self.q = asyncio.Queue(loop=self.loop)
        
        self.run_soon(self.Push)
        
    def send(self, header, message):
        self.run_soon(self.add_to_queue, header, message)
        
    async def add_to_queue(self, header, message):
        await self.q.put((header, message))

    async def Push(self):
        while True:
            header, message = await self.q.get()
            self.log.info('message: %s: %s sent' % (header, message))
            self.pn.send(event=header, description=message)
            
pn = prowl()

def pushNotification(header, message):
    pn.send(header, message)

say (needs pychromecast library and google.cloud TTS api key)

import os, re
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = '/etc/openhab2/HABApp/GoogleTTSAPIKey.json'
import chromecast
from google.cloud import texttospeech

def get_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        # doesn't even have to be reachable
        s.connect(('10.255.255.255', 1))
        IP = s.getsockname()[0]
    except:
        IP = '127.0.0.1'
    finally:
        s.close()
    return IP

class GoogleHome(HABApp.Rule):
    """
        Create a Google home (an host or a devicename is mandatory)
        :param devicename: string : the ip or device name of the Google Home
        :param host: the host of google home
        :param port: the port to contact google home by ip (default is 8009)
        :param ip - ip of your local http server
    """
    
    file_lock = threading.Lock()
    delete_lock = threading.Lock()
    q = {}
    
    def __init__(self, devicename = None, host = None, port = None, ip = None):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)

        self.__runtime = self._Rule__runtime

        if devicename != None:
            chromecasts = pychromecast.get_chromecasts()
            filteredChromeCast = filter(lambda c: c.host == devicename or c.device.friendly_name == devicename , chromecasts)
            
            try:
                self.cc = next(filteredChromeCast)
            except StopIteration:
                availbale_devices = list(map(lambda c: c.device.friendly_name, chromecasts))
                raise ValueError('Unable to found %s device. Available devices : %s'%(devicename, availbale_devices))
        elif host != None:
            self.cc = pychromecast.Chromecast(host, port)
        else:
            raise ValueError('host or devicename is mandatory to create a GoogleHome object.')
            
        #self.log.info('Got Chromecast device')
        
        # get local ip, for http server address
        if ip is None:
            self.ip = get_ip()
        else:
            self.ip = ip
            
        self.name = self.cc.device.friendly_name
        
        #add Queue to class variable self.q for synchronizing multiple GoogleHome instances
        self.loop = self.__runtime.loop
        q = asyncio.Queue(loop=self.loop)
        self.q[self.name] = q
        
        self.file_name_max = 50
        self.basedir = "/var/www/html/"
        self.cachedir = "/mp3_cache/"
        self.dir = os.path.normpath(self.basedir+self.cachedir)
        self.file_name = 'output.mp3'
        self.file_path = os.path.join(self.dir,self.file_name)
        
        self.delete_output()
        self.run_soon(self.saying)
            
    def say(self, text):
        self.run_soon(self.async_say, text)
        
    async def async_say(self, text):
        await self.q[self.name].put(text)
        if not self.delete_lock.locked():
            with self.delete_lock:
                for q in self.q.values():
                    await q.join()
                self.delete_output()
        
    async def saying(self, lang = 'en-GB'):
        while True:
            text = await self.q[self.name].get()
            with self.file_lock:
                ttsurl = await self.googleTTS_builder(text, lang)
            self.log.info('%s: say: %s' % (self.name, text))
            await self.play(ttsurl+'?'+str(random.randint(0,1000)))   #caching issue workaround

    async def play(self, url, contenttype = 'audio/mp3'):
        self.cc.wait()
        mc = self.cc.media_controller
        mc.play_media(url, contenttype)
        mc.block_until_active()
        while mc.status.player_is_idle:
            #self.log.info("%s: waiting for text to start" % self.name)
            await asyncio.sleep(0.1)
        #self.log.debug(mc.status)
        #mc.play()
        while mc.status.player_is_playing:
            #self.log.info("%s: waiting for text to end" % self.name)
            await asyncio.sleep(0.1)
        #mc.stop()
        self.log.info("%s: played url %s duration: %ss" % (self.name, url, mc.status.duration))
        #self.cc.quit_app()
        self.q[self.name].task_done()  
        
    def delete_output(self):
        with self.file_lock:
            self.delete_old_files(30)
            if os.path.exists(self.file_path):
                self.log.debug('deleted: %s' % self.file_path)
                os.remove(self.file_path)
        
    def delete_old_files(self, days = 30):
        now = time.time()
        for f in os.listdir(self.dir):
            file_path = os.path.join(self.dir,f)
            if os.path.getmtime(file_path) < now - (days * 86400):
                if os.path.isfile(file_path):
                    os.remove(file_path)
                    self.log.debug('deleted old TTS file: %s' % file_path)
        
    def filename_from_text(self, text):
        if len(text) > self.file_name_max:
            return self.file_path, self.file_name
        file = re.sub('[^0-9a-zA-Z]', '_', text) + '.mp3'
        return os.path.join(self.dir, file), file
        
    def get_url(self, text):
        file_path, file = self.filename_from_text(text)
        if os.path.exists(file_path):
            os.utime(file_path) #update file access and modified time
            self.log.debug('TTS file found: %s' % (file))
            return "http://"+self.ip+os.path.join(self.cachedir,file)
        return None
        
    async def googleTTS_builder(self, text, lang = "en-GB"):
        url = self.get_url(text)
        if url is not None:
            self.log.debug('found existing url: ...%s' % url[len(url)-20:])
            return url
            
        self.log.debug('Generating TTS file for %s...' % text[:30])
        # Instantiates a client
        client = texttospeech.TextToSpeechClient()

        # Set the text input to be synthesized
        synthesis_input = texttospeech.types.SynthesisInput(text=text)

        # Build the voice request, select the language code ("en-US") and the ssml
        # voice gender ("neutral")
        voice = texttospeech.types.VoiceSelectionParams(
            language_code=lang,
            #language_code='en-GB',
            name='en-GB-Standard-C',    #A and C = Female, B and D are male
            #name="en-GB-Wavenet-A",    #more expensive version of above
            ssml_gender=texttospeech.enums.SsmlVoiceGender.FEMALE)

        # Select the type of audio file you want returned
        audio_config = texttospeech.types.AudioConfig(
            audio_encoding=texttospeech.enums.AudioEncoding.MP3,
            pitch=0.0, sample_rate_hertz=16000, speaking_rate=0.8, volume_gain_db=0.0)

        # Perform the text-to-speech request on the text input with the selected
        # voice parameters and audio file type
        response = client.synthesize_speech(synthesis_input, voice, audio_config)
        
        try:
            os.mkdir(self.dir)
        except:
           pass

        file_path, file = self.filename_from_text(text)
           
        # The response's audio_content is binary.
        with open(file_path, 'wb') as out:
            # Write the response to the output file.
            out.write(response.audio_content)
            
        self.log.debug('TTS file %s written' % file)

        #return serve_file(response.audio_content, "audio/mpeg")
        return "http://"+self.ip+os.path.join(self.cachedir,file)

# Define your chromecasts here
chromecast={'Google Home Upstairs':{
      'ip': 192.168.100.201,
      'port': 8009},
    'Google Mini 1':{
     ' ip': 192.168.100.80,
      'port': 8009}
}
http_server = 'your web server ip here'
ccs=[]
for name, cc in chromecast.items():
    log.debug('loaded chromecast_params: %s: host=%s port=%s http_server:%s' % (name, cc['ip'], cc['port'], http_server))
    ccs.append(GoogleHome(host=cc['ip'], port=cc['port'], ip=http_server))
    
def say(text=''):
    for cc in ccs:
        cc.say(text)

CreateTimer:

from threading import Timer
import time

class createTimer(HABApp.Rule):
    '''
    General timer class using threading.Timer
    '''

    def __init__(self, seconds=-1, exec_func=None, *args, **kwargs):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)
        
        self.t = seconds
        self.exec_func = exec_func
        self.start_time = time.time()
        run = self.kwargs.pop('run', False)
        self.args = args
        self.kwargs = kwargs
        self.timer = Timer(self.t, self.function_to_run)
        if self.t >= 0 and run:
            self.start()
        
    def function_to_run(self):
        if self.exec_func is not None:
            self.exec_func(*self.args, **self.kwargs)
 
    @property
    def is_running(self):
        return self.timer.is_alive()
        
    @property    
    def time_till_run(self):
        if self.is_running:
            return max(0, self.t - (time.time() - self.start_time))
        return 0
        
    def reschedule(self, seconds=None):
        self.start(seconds)
        
    def start(self, seconds=None, *args, **kwargs):
        if seconds is not None:
            self.t = seconds
        self.timer.cancel()
        if self.t >= 0:
            if not args:
                args = self.args
            else:
                self.args = args
            if not kwargs:
                kwargs = self.kwargs
            else:
                self.kwargs = kwargs
            self.start_time = time.time()
            self.timer = Timer(self.t, self.function_to_run)
            self.timer.daemon = True
            self.timer.start()
        
    def cancel(self):
        self.timer.cancel()

CreateTimer is used like this:

class PatioDoor(HABApp.Rule):
    def __init__(self):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)
        
        self.online_t = 15
        self.offline_t = 60
        self.t = self.online_t 
        if self.item['patio_door_online'].value == "Offline":
            self.t = self.offline_t     # time to decide door has not closed properly
                                                         
        self.closed_timer = createTimer(self.t, self.door_not_closed, run=False)

You can then call self.closed_timer.start(), self.closed_timer.cancel() and so on. start() cancels the timer and restarts it, which is useful. This is a thread based timer, I am thinking of making an asyncio based timer, but I’m not sure if it’s worth the effort.

These are just examples, you can make of them what you will

2 Likes

It’s nice to see some code samples posted. :+1:

Just for my understanding - why did you implement a custom time class and did not use “self.run_in” from the rule?

Here is my Timer class using run_in()

import datetime

class createTimer(HABApp.Rule):
    '''
    General timer class
    Restartable Timer, with cancel and reschedule functions
    accepts float as seconds parameter (not claiming that the timer is that accurate though)
    '''
    
    def __init__(self, seconds=0, exec_func=None, *args, **kwargs):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)
        self.t = seconds
        self.exec_func = exec_func
        run = kwargs.pop('run', False)
        self.args = args
        self.kwargs = kwargs
        self._task = self.run_in(self.get_milliseconds, self.exec_func, *args, **kwargs)
        if not run:
            self.cancel()
            
    @property
    def get_milliseconds(self):
        return datetime.timedelta(milliseconds=int(self.t*1000))
   
    @property
    def is_running(self):
        return not self._task.is_finished
        
    @property    
    def time_till_run(self):
        if not self._task.is_finished:
            return (self._task.get_next_call() - datetime.datetime.now()).total_seconds()
        return 0
        
    def reschedule(self, seconds=None):
        self.start(seconds)
        
    def start(self, seconds=None, *args, **kwargs):
        if seconds is not None:
            self.t = seconds
        if self.t >= 0:
            self.cancel()
            if not args:
                args = self.args
            if not kwargs:
                kwargs = self.kwargs
            self._task = self.run_in(self.get_milliseconds, self.exec_func, *args, **kwargs)
           
    def cancel(self):
        self._task.cancel()

The reasons for using a custom timer class, is that I am converting a lot of OH rules to Habapp, and this is how CreateTimer works in OH. it has cancel(), reschedule(), is_running() methods, it also has milliseconds accuracy (or at least it takes milliseconds as an input), so rather than re-writing all the rules, it’s easier to just replace OH CreateTimer with a python version of CreateTimer.

Obviously I have to re-write a lot of the OH rule, but if I keep the calls with similar names, it’s easier to spot mistakes in translation.

Maybe I’ll increase timer resolution, but I’ll first have to take a peek how it affects performance since HABApp is very currently lightweight. It was already requested once in this thread.

Also you can now take parameter files and use them to dynamically set up your rules and even validate the input.

I’m not totally sure that millisecond accuracy is needed, it was just that the CreateTimer function in OH accepts milliseconds, so it was easier to just not mess with the math (until later, when I clean up all my rules).

Having said that I do have some functions that fires commands off to members of group items 100ms apart, using CreateTimer, but that was just to slow down serial data traffic, now I do the same thing but with a Queue, so it’s not a problem anymore.

Here is another example Rule.

This is a first cut (so be kind). It allows you to access persistence data, so that you can use historical data for an item.

import datetime
import json

class GetPersistenceData(HABApp.Rule):

    '''
    Get Historic persistence data for an item
    Uses the current default persistence service defined in OH.
    item_name or an Item instance can be used wherever item_name is specified below.
    
    If instantiated with an item name, or start_update(item_name) is used, persistence will be updated automatically,
    and you can just use the data methods.
    If not you have to call get_all_data(item_name) and wait for valid_data = True before reading any values.
    values will not be updated until the next time you call get_all_data() again
    '''
    
    def __init__(self, persistenceitem=None):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)
        
        self.getting_data = None
        self.json = {}
        self.__runtime = self._Rule__runtime
        self.config = self.__runtime.config
        self.__host = self.config.openhab.connection.host
        self.__port = self.config.openhab.connection.port
        
        self.listen_item = None
        
        self.persistenceitem = persistenceitem
        if self.persistenceitem is not None:
            self.listen_item = self.listen_event(self.persistenceitem, self.item_state_update, ValueUpdateEvent)
        self.get_all_data(persistenceitem)
        
    def item_state_update(self, event):
        self.getting_data = None
        self.get_all_data()
        
    def start_update(self, persistenceitem=None):
        if persistenceitem is not None:
            self.persistenceitem = self.get_name(persistenceitem)
        if self.listen_item is not None:
            self.listen_item.cancel()
        if self.persistenceitem is not None:
            self.listen_item = self.listen_event(self.persistenceitem, self.item_state_update, ValueUpdateEvent)
            
    def stop_update(self):
        if self.listen_item is not None:
            self.listen_item.cancel()
        
    def set_item(self, persistenceitem):
        self.persistenceitem = self.get_name(persistenceitem)
   
    def get_name(self, persistenceitem):
        if isinstance(persistenceitem, str):
            return persistenceitem
        elif isinstance(persistenceitem, HABApp.core.items.Item):
            return persistenceitem.name
            
        return None
            
    def get_all_data(self, persistenceitem=None):
        if persistenceitem is not None:
            self.persistenceitem = self.get_name(persistenceitem)
        if self.persistenceitem is not None:
            self.sendHttpGetRequest()
            
    def get_data_range(self, firstdate, lastdate=None, persistenceitem=None):
        #dates are datetime objects
        if lastdate is None:
            lastdate = datetime.datetime.now()
            
        if (lastdate - firstdate).total_seconds() < 0:
            self.log.error('lastdate: %s must be earlier than firstdate %s' %(lastdate, firstdate))
            return []
            
        if persistenceitem is None and self.persistenceitem is None:
            self.log.error('You must specify an item to obtain data on')
            return []
        
        before_list = self.get_list_before_date(lastdate)
        return self.get_list_after_date(firstdate, before_list)

    def get_previous_value(self, persistenceitem=None):
        if persistenceitem is None and self.persistenceitem is None:
            self.log.error('You must specify an item to obtain data on')
            return None
        
        try:
            return data_values[-1]['state']
        except IndexError:
            return None
            
    def max(self, firstdate, lastdate=None, persistenceitem=None):
        data = self.get_data_range(firstdate, lastdate, persistenceitem)
        try:
            return max([float(value['state']) for value in data])
        except ValueError:
            return None
        
    def min(self, firstdate, lastdate=None, persistenceitem=None):
        data = self.get_data_range(firstdate, lastdate, persistenceitem)
        try:
            return min([float(value['state']) for value in data])
        except ValueError:
            return None
        
    def average(self, firstdate, lastdate=None, persistenceitem=None):
        data = self.get_data_range(firstdate, lastdate, persistenceitem)
        data_points = len(data)
        try:
            return sum([float(value['state']) for value in data])/data_points
        except (ValueError, ZeroDivisionError):
            return None
        
    def values_since(self, firstdate, lastdate=None, persistenceitem=None):
        data = self.get_data_range(firstdate, lastdate, persistenceitem)
        try:
            return [float(value['state']) for value in data]
        except ValueError:
            return None
            
    def get_list_after_date(self, date, data_list=None):
        if data_list is None:
            data_list = self.data_values
        if data_list:
            target_timestamp = datetime.datetime.timestamp(date)*1000
            return [ j for j in data_list if j['time'] >= target_timestamp ]
        return []

    def get_list_before_date(self, date, data_list=None):
        if data_list is None:
            data_list = self.data_values
        if data_list:
            target_timestamp = datetime.datetime.timestamp(date)*1000
            return [ j for j in data_list if j['time'] <= target_timestamp ]
        return []
        
    @property
    def data_values(self):
        # eg [{"time":1575561360000,"state":"1662.0234666666665"},{"time":1575561600000,"state":"1753.0922208333334"}]
        return self.json.get('data', [])
    
    @property
    def num_datapoints(self):
        return int(self.json.get("datapoints", 0))
        
    @property
    def item_name(self):
        return self.json.get('name', self.persistenceitem)
        
    @property
    def valid_data(self):
        if self.getting_data is None:
            return False
        return self.getting_data.is_finished
        
    @property
    def updating(self):
        return self.listen_item is not None
        
    def get_openhab_url(self, url, *args, **kwargs):
        assert not url.startswith('/')
        url = url.format(*args, **kwargs)
        return f'http://{self.__host:s}:{self.__port:d}/{url:s}'
        
    def sendHttpGetRequest(self):
        self.getting_data = self.run_soon(self._sendHttpGetRequest)
            
    async def _sendHttpGetRequest(self):
        url = self.get_openhab_url("rest/persistence/items/%s" % self.persistenceitem)
        self.log.debug('Getting persistence values for %s' % self.persistenceitem)
        async with self.async_http.get(url) as resp:
            #self.log.debug(resp)
            if resp.status != 200:
                log.info.error('Could not get persistence data for: %s' % self.persistenceitem)
                self.getting_data = None
                return
            self.response = await resp.text()
            self.json = json.loads(self.response)
            #self.log.debug(self.response)

You use it like this:

class HEMAverageValues(HABApp.Rule):
    
    def __init__(self):
        super().__init__()
        self.log = logging.getLogger('MyRule.'+self.__class__.__name__)
        
        self.power = Item.get_item('HEM_P')
        self.persistance = GetPersistenceData(self.power)
        self.register_on_unload(self.persistance.stop_update)
        
        # Trigger on item updates
        self.listen_event(self.power, self.item_value_changed, ValueChangeEvent)
        
    def item_value_changed(self, event):
        self.log.info('Item %s received update %s' % (event.name, event.value))
        self.get_average_values()
        
    def post_update(self, item, value):
        self.oh.post_update(item, value)
        
    def get_average_values(self):
        self.log.info('calculating new min/max/adv')
        prev_24_hours = datetime.datetime.now() - datetime.timedelta(hours = 24)
        try:
            PowerAverage = round(self.persistance.average(prev_24_hours) / 1000, 2)     #(kW)
            PowerMaximum = round(self.persistance.max(prev_24_hours) / 1000, 2)         #(kW)
            PowerMinimum = round(self.persistance.min(prev_24_hours) / 1000, 2)         #(kW)

            self.log.info("Energy - 24 hr Average: %skW, Max: %skW, Min: %skW" % (PowerAverage, PowerMaximum, PowerMinimum))
            
            self.post_update('Energy_Average24h', PowerAverage)
            self.post_update('Energy_Max24h', PowerMaximum)
            self.post_update('Energy_Min24h', PowerMinimum)
        except TypeError as e:
            self.log.error('error: %s' % e)

HEMAverageValues()

Output looks like this (I do some other calculations, so there is a bit more data in this output, but you get the idea):

2019-12-06 15:40:13,603[    MyRule.HEMCaculateCost] INFO|  item_value_changed: Item HEM_P received update 1760.443
2019-12-06 15:40:13,604[    MyRule.HEMCaculateCost] INFO|    calc_energy_cost: Energy - Current kW being used: 1.760443
2019-12-06 15:40:13,604[    MyRule.HEMCaculateCost] INFO|    calc_energy_cost: Energy - Current kWh used since last update: 0.014278777941839167
2019-12-06 15:40:13,609[    MyRule.HEMCaculateCost] INFO|    calc_energy_cost: Energy - Instantaneous Energy Cost is: 1.760kWh @14.4c/kWh (Mid-peak) = 25.35c/h
2019-12-06 15:40:13,614[    MyRule.HEMCaculateCost] INFO|  get_average_values: Energy - 24 hr Average: 1.97kW, Max: 5.06kW, Min: 1.36kW
2 Likes

Ah nice, accessing the persistence service is definitely on my list.
For small things I currently use the statistics utility.

Hey have seen HABApp for the first time today. Looks really promising! Despite scanning the thread and looking at the docs i can not really grasp what the pros and cons compared to jython jsr with the helper libraries are. Is it already on par “competing” with jython?

As far as i understood the biggest pro is using real python3 with the ability to import all modules instead of jython 2.x. which i would like a lot. I have the feeling that jsr jython will never really make it into openhab (because the maintainers don’t want it to) staying an outcast forever. Therefore I wouldn’t mind using a solution for my rules running outside OH itself.
What cons may i have to face when using HABApp? Rule creation looks more difficult to me since the helper libraries use nice decorators.

I am not an programming expert (just do it for fun) and have all my rules running with jsr and the helper libs.

I do not mean any disrespect to HABApp or the effort Sebastian has put into it, but the reality is that HABApp is nowhere near, and cannot ever be close, to the capabilities of the new rule engine with scripted automation (what you refer to as jython jsr). HABApp is crippled by only acessing OH through the REST API. Scripted automation has full access to OH and any other Java or Python2 libraries that you’d like to pull in. The limitation with scripted automation is that Jython is currently at a level of Python 2.7. With OH3, GraalVM will be integrated into OH and remove the need for Jython, so you will be able to use whichever version of Python you’d like… or many other supported languages.

1 Like

Thanks for your detailed answer :slight_smile:

Always depends on which way you look at it. I really tried the jsr engine, I even was the biggest supporter since openhab1 but I always felt limited by it - something I don’t have with HABApp.

Like PaperUI, Habmin, etc… If it’s possible to do the whole openhab configuration through it I’d not use the word “crippled”. But the wording shows you never tried it.

Yea - but python2 is end of life and many libraries don’t support it any more. Also you can’t use compiled resources of libraries and on top you have to get along with the jsr223 quirks.
With HABApp you can install any python library you like and it will work without a hassle.

While this might be true in some distant future I need some good automation framework now since I am now trying to automate stuff. Also I doubt any 3rd party implementation will always be as feature complete as the reference python implementation.

I don’t force anyone to use something I wrote for me but decided to share because I see lots of people with similar problems. I just point out that there are more elegant solutions than using curl to do a request or use the exec binding to start a python script.

And the nice thing is, you can use HABApp how you like, it doesn’t get in your way.
Use jsr automation and do one thing you struggle with HABApp - no problem.
Gradually migrate rules - no problem.
Quickly try rules from your main machine which isn’t running openhab without breaking anything - no problem.

2 Likes

Version 0.11.0

Changelog:

  • Added native support for sunrise/sunset
  • Added support for ThingStatus events
  • Updated documentation and added getting started sectio
  • Internal Timestamps are all utc now
  • Added location config
  • Added fixes and tests for QuantityType

The scheduler allows now more complex statements out of the box:

        # run the function on sunrise
        ret = self.run_on_sun('sunrise', self.on_sunrise)
        
        # trigger will not be earlier than 7:30
        ret.earliest(time(7, 30))
        
        # trigger will not be later than 9:00
        ret.latest(time(9, 00))
        
        # add a 1h offset to the calculated sunrise time
        ret.offset(timedelta(h=1))
        
        # add a random value in the interval +- 300 secs to sunrise time
        ret.jitter(300)
1 Like

I agree with Sebastian. Having all of python 3 available, plus all the libraries is a big benefit. You can also restart the rules engine without touching OH - which is also a big plus.
You can debug one rule without taking your whole system down. Error messages are easy to understand, and specific.

I have converted all of my rules over to HABApp (over 600 of them), and everything works perfectly. I haven’t run into any limitations. Anyway, none of this precludes you from using the regular rules engine in conjunction if you need to.

I’ll take HABApp now, over any future possibilities for OH 3.0. When 3.0 comes out, then we’ll see what’s possible or not. Many people will likely stay with OH 2.5 due to the removal of 1.x bindings in 3.0, so I think what Sebastian has done here will have a benefit well into the future.

2 Likes

Hello Sebastian,
thank you for your work and your willingness to share it with us!
Can you give me a suggestion on how to develop and debug my rules? I would like to run HABApp in a docker container.