How to update this AppDaemon 3 script to AD 4

Tags: #<Tag:0x00007f73b3274d30>

please let me seek assistance here. Until 115 I still had AD3 running and working perfectly, with below 2 automations. AD3 now no longer works, and what’s more, it is gone from the Add-on store?

Ill post these 2 scripts here hoping any one of you guru’s is able to re-write them for AD 4

they are very useful scripts:

the first adds geocoded location info to life360 sensors (I dont use the App sensors), and the second is a last_motion automation, listing the last recorded motion sensor.

Hope you can help:

Geocoded sensors, address.py

"""Address AppDaemon app."""
# pylint: disable=attribute-defined-outside-init, unused-argument, too-many-arguments
import appdaemon.plugins.hass.hassapi as hass
#
# Address App
# requires: "geopy" https://pypi.org/project/geopy/
#
#
# Args:
#   entity: entity_id of a device_tracker entity, example "device_tracker.my_entity"
#

class Address(hass.Hass):
    """Address class."""

    def initialize(self):
        """initialize Address."""
        self.log("App started.")
        entity_config = self.args["entity"]

        if isinstance(entity_config, str):
            entities = []
            entities.append(entity_config)
        else:
            entities = entity_config

        for entity in entities:
            self.listen_state(self.get_address, entity)
            self.log("State listener for {} started.".format(entity))

    def get_address(self, entity, attribute, old, new, kwargs):
        """Set the state + attributes of a defined device_tracker entity."""
        from geopy.geocoders import Nominatim
        geo = Nominatim(user_agent="AppDaemon")
        lat = self.get_state(entity, attribute="latitude")
        long = self.get_state(entity, attribute="longitude")

        if lat is None or long is None:
            self.log("{} does not have lat/long attributes.".format(entity))
            return

        lat_long = "{}, {}".format(lat, long)

        data = geo.reverse(lat_long)
        raw = data.raw["address"]
        attributes = self.get_state(entity, attribute="all")["attributes"]

        for attr in raw:
            attributes[attr] = raw[attr]

        self.log("Updating state for {}".format(entity))
        self.set_state(entity, attributes=attributes)

and binary_sensors recording multi_bin_sensor_history.py:

# -*- coding: utf-8 -*-
"""
Binary sensor custom history as AppDaemon App for Home Assistant.

Listen to multiple binary sensors to reflect the last activation,
maintaining a history of the lasts activations.

* Sensor State: `Friendly name`
* Sensor attributes:
    `history_1: Friendly name: activation_timestamp`,
    `history_2: Friendly name: activation_timestamp`,
    ...

Yaml config goes like this:


LastMotionHistory:
  class: MultiBinSensor
  module: multi_bin_sensor_history
  new_entity: sensor.last_motion
  max_history: 10
  binary_sensors:
    binary_sensor.hue_motion_sensor_1_motion: Cocina
    binary_sensor.hue_motion_sensor_2_motion: Office
  format_last_changed: '%H:%M:%S'
  icon: mdi:motion-sensor
  friendly_name: Last motion


"""
from collections import deque
from typing import Deque, Dict

import appdaemon.plugins.hass.hassapi as hass

_ATTR_NAME = "history_"


class MultiBinSensor(hass.Hass):

    _entity: str
    _date_format: str
    _history: Deque[str]
    _friendly_names: Dict[str, str]
    _entity_attributes: Dict[str, str]

    def initialize(self):
        """AppDaemon required method for app init."""
        self._entity = self.args.get("new_entity")
        icon = self.args.get("icon", "mdi:motion-sensor")
        friendly_name = self.args.get("friendly_name", "Last motion")
        self._entity_attributes = {"icon": icon, "friendly_name": friendly_name}
        self._date_format = self.args.get("format_last_changed")

        # Set up state history for attributes
        self._history = deque([], maxlen=int(self.args.get("max_history")))

        # Listen for binary sensor activations and store friendly names for them
        bin_sensors: Dict[str, str] = self.args.get("binary_sensors")
        self._friendly_names = {}
        for sensor, pretty_name in bin_sensors.items():
            self._friendly_names[sensor] = pretty_name
            self.listen_state(self._bin_sensor_activation, sensor, new="on")

        # recover old values, if any
        old_attrs = self.get_state(self._entity, attribute="all")
        if old_attrs:
            state = old_attrs.get("state", "unknown")
            for k, old_value in reversed(old_attrs.get("attributes", {}).items()):
                if k.startswith(_ATTR_NAME) and ": " in old_value:
                    self._history.append(old_value)
            # Re-Publish old state
            self._set_new_sensor_state(state)

    def _set_new_sensor_state(self, state):
        """Publish a new state for the sensor."""
        history_attrs = {
            f"{_ATTR_NAME}{i}": old_state
            for i, old_state in enumerate(reversed(self._history))
            if i > 0
        }
        attributes = {**self._entity_attributes, **history_attrs}
        self.set_state(self._entity, state=state, attributes=attributes)

    def _bin_sensor_activation(self, entity, attribute, old, new, kwargs):
        """Listen to bin sensors turning on, update history and publish a new state."""
        location = self._friendly_names[entity]
        pretty_date_now = self.datetime().strftime(self._date_format)

        # Add to history
        self._history.append(f"{location}: {pretty_date_now}")

        # Publish new state
        self._set_new_sensor_state(location)

apps.yaml
configs for both scripts:

# https://github.com/ludeeus/ad-address
# https://community.home-assistant.io/t/how-to-iterate-over-a-timezone-list-in-python-to-find-country-code/123262/53
address:
  module: address
  class: Address
  entity:
    - device_tracker.life360_marijn
    - device_tracker.life360_w
    - device_tracker.life360_d1
    - device_tracker.life360_d2
    - device_tracker.life360_d3
    - device_tracker.life360_d4

and

LastMotionHistory:
  class: MultiBinSensor
  module: multi_bin_sensor_history
  new_entity: sensor.last_motion
  max_history: 10
  binary_sensors:
    binary_sensor.laundry_sensor_motion: Laundry
    binary_sensor.dining_table_sensor_motion: Dining
    binary_sensor.auditorium_sensor_motion: Auditorium
    binary_sensor.frontdoor_sensor_motion: Frontdoor
    binary_sensor.library_sensor_motion: Library
    binary_sensor.corridor_sensor_motion: Corridor
    binary_sensor.corridor_terrace_sensor_motion: Corridor terrace
    binary_sensor.master_bedroom_sensor_motion: Master bedroom
    binary_sensor.corridor_office_sensor_motion: Corridor office
    binary_sensor.front_room_sensor_motion: Front room
    binary_sensor.guest_room_sensor_motion: Guest room
    binary_sensor.dorm_sensor_motion: Dorm
    etc etc etc


  format_last_changed: '%H:%M:%S'
  friendly_name: Last motion

thanks for having a look! Much appreciated.

From the first look, I thi k it should be not a big issue to update these to AppDaemon 4. I’ll take a deeper look at it this weekend and hope that I’ll be able to help you out. Just a warning, I’m only a maybe “advanced” programmer, so it’ll probably take me some time.

would be appreciated!

any thought on the bigger issue of AD3 not being supported anylonger? Hasnt been a breaking change…

edit
tried AD 4 just now, but that wont start successfully either…?

probably a user error…

system_packages: []
python_packages:
  - geopy
init_commands: []


in the config
this is the error log:

[16:14:03] INFO: Starting AppDaemon...
ERROR 'log' directive deprecated, please convert to new 'logs' syntax

separate post to clarify:

updated the new way of config for appdaemon.yaml, so now it loads successfully, and even the multiple binary sensor app runs and creates sensor.last_motion! wow.

my address.py doesn’t work yet, though it logged to be listening to the correct device_trackers.

So please only check that part of the post… sorry for the confusion.

Glad that you got the last motion app working. Try this for the address app:

"""Address AppDaemon app."""
from geopy.geocoders import Nominatim

import adbase as ad
#
# Address App
# requires: "geopy" https://pypi.org/project/geopy/
#
# Args:
#   entities: list of entity_ids to track, accepted formats are string, comma separated list and list
#

class Address(ad.ADBase):
    """Address class."""

    def initialize(self):
        """initialize."""
        self.adbase = self.get_ad_api()
        self.hass = self.get_plugin_api("HASS")

        self.adbase.log("App started.")
        entities = self.args["entities"]

        if isinstance(entities, str):
            entities = [ent_id.strip() for ent_id in entities.split(",")]

        for entity in entities:
            self.hass.listen_state(self.get_address, entity)
            self.adbase.log(f"State listener for {entity} started.")

    def get_address(self, entity, attribute, old, new, kwargs):
        """Add address information to device tracker."""
        attributes = self.hass.get_state(entity, attribute="all")["attributes"]
        latitude = attributes.get("latitude")
        longitude = attributes.get("longitude")

        if not latitude and not longitude:
            self.adbase.log(f"No latitude/longitude attribute found for {entity}")
            return

        # Get address
        geo = Nominatim(user_agent="AppDaemon")
        data = geo.reverse(f"{latitude}, {longitude}")
        raw = data.raw["address"]

        for attr in raw:
            attributes[attr] = raw[attr]

        self.adbase.log(f"Updating state for {entity}")
        self.hass.set_state(entity, attributes=attributes)

thanks, unfortunately this is in the log:

2020-09-19 23:27:00.051145 INFO address: App started.
2020-09-19 23:27:00.053427 WARNING address: ------------------------------------------------------------
2020-09-19 23:27:00.054602 WARNING address: Unexpected error running initialize() for address
2020-09-19 23:27:00.055401 WARNING address: ------------------------------------------------------------
2020-09-19 23:27:00.056656 WARNING address: Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/appdaemon/app_management.py", line 150, in initialize_app
    await utils.run_in_executor(self, init)
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 290, in run_in_executor
    response = future.result()
  File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/appdaemon/apps/address.py", line 22, in initialize
    entities = self.args["entities"]
KeyError: 'entities'
2020-09-19 23:27:00.057272 WARNING address: ------------------------------------------------------------

while before, at least I had it list the correct entities is listened too…

Can you please reply to me next time? I just found it out by accident that you answered.

Yeah sorry, I changed the app quite a bit as it was a bit innefficient.
You need to use entities in your yaml cofdig for the app, e.g.

address:
  class: address
  module: Address
  entities: device_tracker.xy

Entities can be a list, a comma separated string of entities or a single entity.

yes! that’s progress!
I can now see the successful startup of the app, and even the correct address data being gathered. There’s an error though on the state not being specified:

2020-09-20 10:50:13.662696 INFO address: Updating state for device_tracker.life360_marijn
2020-09-20 10:50:16.007713 WARNING HASS: Error setting Home Assistant state default.device_tracker.life360_marijn, {'attributes': {'source_type': 'gps', 'latitude': redacted, 'longitude': redacted, 'gps_accuracy': 17, 'battery': 89, 'address': 'Home', 'at_loc_since': '2020-09-20T08:50:08+00:00', 'battery_charging': False, 'driving': False, 'last_seen': '2020-09-20T08:50:08+00:00', 'moving': False, 'place': 'Home', 'raw_speed': 0.28506767749786377, 'speed': 1, 'wifi_on': True, 'friendly_name': 'Life360 Marijn', 'entity_picture': 'https://www.life360.com/img/user_images/redacted.jpg?fd=2', 'hide_attributes': ['templates', 'editable', 'icon_color'], 'house_number': 'redacted', 'road': 'redacted', 'quarter': 'redacted', 'town': 'redacted', 'state': 'Noord-Brabant', 'country': 'Nederland', 'postcode': 'redacted', 'country_code': 'nl'}}
2020-09-20 10:50:16.009816 WARNING HASS: Code: 400, error: {"message": "No state specified."}

Hope this makes sense?

Change the last line to this and try again.

self.hass.set_state(entity, new, attributes=attributes)

no more error. Log now ends with:

2020-09-20 11:10:38.121915 INFO AppDaemon: App initialization complete

Dont see anything else though either. Maybe lfe360 didnt report a state change yet. But, before, when I wanted to make sure the AD app ran correctly, I merely had to open the Life360 app on my phone and cause a refresh for the AD app to do its work.

Could this be something you changed in the AD app? Or maybe Life360 got more efficient not updating when no movement was noted.

Still, I would hope a fresh startup would ‘read’ the address data to being with?

I’d assume the latter, I didn’t change how the code works, I merely organized and optimised it a bit.

I thought of this as well, as the original app also didn’t have this. I can add a few lines to update the address when the app is started as qell.

here’s a clue:

2020-09-20 11:43:05.253687 WARNING address: ------------------------------------------------------------
2020-09-20 11:43:05.254599 WARNING address: Unexpected error in worker for App address:
2020-09-20 11:43:05.255613 WARNING address: Worker Ags: {'id': 'redacted', 'name': 'address', 'objectid': 'redacted', 'type': 'state', 'function': <bound method Address.get_address of <address.Address object at 0xb58ca7f0>>, 'attribute': 'state', 'entity': 'device_tracker.life360_w', 'new_state': 'home', 'old_state': 'Lidl', 'pin_app': True, 'pin_thread': 0, 'kwargs': {'__thread_id': 'thread-0'}}
2020-09-20 11:43:05.256418 WARNING address: ------------------------------------------------------------
2020-09-20 11:43:05.258388 WARNING address: Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/appdaemon/threading.py", line 900, in worker
    funcref(
  File "/config/appdaemon/apps/address.py", line 53, in get_address
    self.hass.set_state(entity, new, attributes=attributes)
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 195, in inner_sync_wrapper
    f = run_coroutine_threadsafe(self, coro(self, *args, **kwargs))
TypeError: set_state() takes 2 positional arguments but 3 were given
2020-09-20 11:43:05.259352 WARNING address: ------------------------------------------------------------

I’m sorry, I’m not in front of a computer right now, try this

self.hass.set_state(entity, state=new, attributes=attributes)

no worries, take time to fiddle with this :wink: glad for any assistance you can give me here.

and yes, I did try that edit already, again, no error, even after a full restarts of the AD add-on, but also, no address data added to the life_360 sensors attributes (yet)

Back at the workstation.

I modified the app, now it sets the address whenever the app is started and then again on every change of the device tracker.

It worked fine for me and successfully added the attributes from geopy to the device tracker. Can you please test?

"""Address AppDaemon app."""
# pylint: disable=attribute-defined-outside-init, unused-argument, too-many-arguments
from geopy.geocoders import Nominatim

import adbase as ad
#
# Address App
# requires: "geopy" https://pypi.org/project/geopy/
#
#
# Args:
#   entity: entity_id of a device_tracker entity, example "device_tracker.my_entity"
#

class Address(ad.ADBase):
    """Address class."""

    def initialize(self):
        """initialize Address."""
        self.adbase = self.get_ad_api()
        self.hass = self.get_plugin_api("HASS")

        self.adbase.log("App started.")
        entities = self.args["entities"]

        if isinstance(entities, str):
            entities = [ent_id.strip() for ent_id in entities.split(",")]

        for entity in entities:
            self.update_address(entity)
            self.hass.listen_state(self.location_change_cb, entity)
            self.adbase.log(f"State listener for {entity} started.")

    def location_change_cb(self, entity, attribute, old, new, kwargs):
        """Update the address on change of location"""
        self.update_address(entity)

    def update_address(self, entity):
        entity_state = self.hass.get_state(entity, attribute="all")
        state = entity_state["state"]
        attributes = entity_state["attributes"]

        latitude = attributes.get("latitude")
        longitude = attributes.get("longitude")

        if not latitude and not longitude:
            self.adbase.log(f"No latitude/longitude attribute found for {entity}")
            return
        
        # Get address
        geo = Nominatim(user_agent="AppDaemon")
        data = geo.reverse(f"{latitude}, {longitude}")
        raw = data.raw["address"]

        for attr in raw:
            attributes[attr] = raw[attr]

        self.adbase.log(f"Updating state for {entity}")
        self.hass.set_state(entity, state=state, attributes=attributes)

thanks but no, nothing but the regular attributes I am afraid:

odd thing, as posted above I had a version which showed the address data completely. Right now the logs only show

and even when is says ‘updating state’ it doesnt add any of the attributes geopy should have found (they aren’t displayed I the log either)

Can you please show you appdaemon 4 config, appdaemon.yaml?

I think it’s a namespace issue, as it is working fine for me.

secrets: /config/secrets.yaml
appdaemon:
  latitude: !secret latitude
  longitude: !secret longitude
  elevation: !secret elevation
  time_zone: !secret time_zone
  plugins:
    HASS:
      type: hass
http:
  url: http://hassio.local:5050
hadashboard:
admin:
api:

haven’t changed that though after I saw the correct data come in earlier…

I just wanted to see your plugin configuration.

Try this:

"""Address AppDaemon app."""
# pylint: disable=attribute-defined-outside-init, unused-argument, too-many-arguments
from geopy.geocoders import Nominatim

import adbase as ad
#
# Address App
# requires: "geopy" https://pypi.org/project/geopy/
#
#
# Args:
#   entity: entity_id of a device_tracker entity, example "device_tracker.my_entity"
#

class Address(ad.ADBase):
    """Address class."""

    def initialize(self):
        """initialize Address."""
        self.adbase = self.get_ad_api()
        self.hass = self.get_plugin_api("HASS")

        self.adbase.log("App started.")
        entities = self.args["entities"]

        if isinstance(entities, str):
            entities = [ent_id.strip() for ent_id in entities.split(",")]

        for entity in entities:
            self.update_address(entity)
            self.hass.listen_state(self.location_change_cb, entity)
            self.adbase.log(f"State listener for {entity} started.")

    def location_change_cb(self, entity, attribute, old, new, kwargs):
        """Update the address on change of location"""
        self.update_address(entity)

    def update_address(self, entity):
        entity_state = self.hass.get_state(entity, attribute="all")
        state = entity_state["state"]
        attributes = entity_state["attributes"]

        latitude = attributes.get("latitude")
        longitude = attributes.get("longitude")

        if not latitude and not longitude:
            self.adbase.log(f"No latitude/longitude attribute found for {entity}")
            return
        
        # Get address
        geo = Nominatim(user_agent="AppDaemon")
        data = geo.reverse(f"{latitude}, {longitude}")
        raw = data.raw["address"]
        self.adbase.log(raw)

        for attr in raw:
            attributes[attr] = raw[attr]

        self.adbase.log(f"Updating state for {entity}")
        self.hass.set_state(entity, state=state, attributes=attributes, namespace="hass")

I added a log which shows the address before updating the state as well.

interesting :wink:
Main attributes are found and listed, but my device_tracker is not found??

2020-09-20 14:59:01.028979 INFO address: App started.
2020-09-20 14:59:01.380038 INFO address: {'house_number': 'redacted', 'road': 'redacted', 'quarter': 'redacted', 'town': 'redactedl', 'state': 'Noord-Brabant', 'country': 'Nederland', 'postcode': 'redacted', 'country_code': 'nl'}
2020-09-20 14:59:01.384911 INFO address: Updating state for device_tracker.life360_marijn
2020-09-20 14:59:01.389414 WARNING address: address: Entity device_tracker.life360_marijn not found in namespace hass
2020-09-20 14:59:01.393737 WARNING address: ------------------------------------------------------------
2020-09-20 14:59:01.394873 WARNING address: Unexpected error running initialize() for address
2020-09-20 14:59:01.395861 WARNING address: ------------------------------------------------------------
2020-09-20 14:59:01.400312 WARNING address: Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/appdaemon/app_management.py", line 150, in initialize_app
    await utils.run_in_executor(self, init)
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 290, in run_in_executor
    response = future.result()
  File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/appdaemon/apps/address.py", line 30, in initialize
    self.update_address(entity)
  File "/config/appdaemon/apps/address.py", line 60, in update_address
    self.hass.set_state(entity, state=state, attributes=attributes, namespace="hass")
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 195, in inner_sync_wrapper
    f = run_coroutine_threadsafe(self, coro(self, *args, **kwargs))
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 299, in run_coroutine_threadsafe
    result = future.result(self.AD.internal_function_timeout)
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 439, in result
    return self.__get_result()
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 388, in __get_result
    raise self._exception
  File "/usr/lib/python3.8/site-packages/appdaemon/adapi.py", line 1440, in set_state
    return await self.AD.state.set_state(self.name, namespace, entity, **kwargs)
  File "/usr/lib/python3.8/site-packages/appdaemon/state.py", line 471, in set_state
    if entity in self.state[namespace]:
KeyError: 'hass'
2020-09-20 14:59:01.400909 WARNING address: ------------------------------------------------------------