Looking for a mentor to help me build a new plugin

Posted on
Sun Sep 05, 2021 7:57 pm
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Looking for a mentor to help me build a new plugin

I'd like to build my first plugin based on what I think is a strong concept with life360. I'd like someone to work with to guide me through the process so I can learn and build a solid plugin. and then build more in the future. I have a working script that can call the API and populate Indigo variables.

Here's the concept and you can let me know if you want to help:

Plugin Config:
- user login
- user password
- authorization_token
- user update frequency (choice to limit API calls)

Devices:
- Choose a user from the list of members that come back from the account

Device States:
- User Name
- User Lat
- User Long
- User Battery Level
- User Phone number
- User Circle #is the user in an established circle ? if so, list it
- User ID # life360 user unique identifier
- User location # address closest to the user based on the geopy API


There is a lot more information available in the API. Once I learn how to write a plugin I can prob add it myself

Posted on
Mon Sep 06, 2021 11:45 am
neilk offline
Posts: 715
Joined: Jul 13, 2015
Location: Reading, UK

Re: Looking for a mentor to help me build a new plugin

I don’t pretend to have the skills to mentor anyone but I have built a couple of plugins that sound like they use many of the same concepts. Happy to help if I can, and let you into the approach I used.

Neil

Posted on
Mon Sep 06, 2021 12:55 pm
jay (support) offline
Site Admin
User avatar
Posts: 18220
Joined: Mar 19, 2008
Location: Austin, Texas

Re: Looking for a mentor to help me build a new plugin

A good place to start is the example custom device in the SDK, and the plugin developer's guide. Once you've dug into those, feel free to ask questions and I'm sure there will be plenty of answers!

Jay (Indigo Support)
Twitter | Facebook | LinkedIn

Posted on
Mon Sep 06, 2021 2:25 pm
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Re: Looking for a mentor to help me build a new plugin

jay (support) wrote:
A good place to start is the example custom device in the SDK, and the plugin developer's guide. Once you've dug into those, feel free to ask questions and I'm sure there will be plenty of answers!


Good resources. I'll give it a shot

Posted on
Mon Sep 06, 2021 2:26 pm
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Re: Looking for a mentor to help me build a new plugin

neilk wrote:
I don’t pretend to have the skills to mentor anyone but I have built a couple of plugins that sound like they use many of the same concepts. Happy to help if I can, and let you into the approach I used.

Neil


Thanks Neil. I'll try the same and let you know when I run into issues! .

Posted on
Tue Sep 07, 2021 4:33 am
neilk offline
Posts: 715
Joined: Jul 13, 2015
Location: Reading, UK

Re: Looking for a mentor to help me build a new plugin

If you want to look at a really simple example (about as simple as it gets) of a plugin that gets states from an API take a look at

https://github.com/neilkplugins/Solcast-indigo-plugin

Which was really a minimum viable version of a plugin I wrote when I was investigating solar panels. It doesn't have any mechanism to manage the token or even automate the refreshes as it does it via an action but it may be a good place to start, and then we can add those capabilities. (The GlowMarkt plugin I wrote does both of those, but as a starting point it may be a good first step from your existing scripts. Feel free to modify it (and I noticed a couple of things I need to updated as I cut n pasted some stuff from my Octopus Energy plugin which I will do).

It is about as simple as you get for a plugin that calls a JSON based API and updates device states.

Happy to explain anything that catches you out, then we can move on to the additional functionality.

Neil

Posted on
Tue Sep 07, 2021 3:26 pm
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Re: Looking for a mentor to help me build a new plugin

neilk wrote:
If you want to look at a really simple example (about as simple as it gets) of a plugin that gets states from an API take a look at

https://github.com/neilkplugins/Solcast-indigo-plugin

Which was really a minimum viable version of a plugin I wrote when I was investigating solar panels. It doesn't have any mechanism to manage the token or even automate the refreshes as it does it via an action but it may be a good place to start, and then we can add those capabilities. (The GlowMarkt plugin I wrote does both of those, but as a starting point it may be a good first step from your existing scripts. Feel free to modify it (and I noticed a couple of things I need to updated as I cut n pasted some stuff from my Octopus Energy plugin which I will do).

It is about as simple as you get for a plugin that calls a JSON based API and updates device states.

Happy to explain anything that catches you out, then we can move on to the additional functionality.

Neil


thanks so much !

Posted on
Wed Sep 08, 2021 6:53 am
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Re: Looking for a mentor to help me build a new plugin

First attempt is here: https://github.com/ryanbuckner/life360-plugin

I was able to configure the shell and get the plugin to allow devices to be created. During device config, it goes out to life 360 and brings back your members to select for the device.

The idea is that each device will represent a person in the Circle, and that will contain the states.

A few things I need help with next :

- how to pick the right device for state updates? (for example, if the api call brings back data for 3 members of the family, how to update the indigo device for the right associated family member)
- right now the plugin uses a config file to store authentication information. I put some fields in the plugin config to capture them, but I'm not using them

Posted on
Wed Sep 08, 2021 11:49 am
mundmc offline
User avatar
Posts: 1060
Joined: Sep 14, 2012

Re: Looking for a mentor to help me build a new plugin

Kudos!
I got nothing to add here, but kudos!

Posted on
Wed Sep 08, 2021 1:30 pm
neilk offline
Posts: 715
Joined: Jul 13, 2015
Location: Reading, UK

Re: Looking for a mentor to help me build a new plugin

OK - to help me get my head around the device updates could you post what the API returns and we can figure out how to split this out by device. That may involve shifting to add in the update mechanisms from another plugin.

In the mean time we can look to use the plugin config details rather than the config file. I don't know anything about life360, but you have a choice to use one set of login credential globally for the plugin and have devices created per family member or have the device associated with a separate account and family member. It depends if you would ever need to access more than one account.

That will could how we implement it but given you have added the config items you can use

Code: Select all
self.pluginPrefs['life360_username']
in your api call (and the same for token and password) and then you can remove the config file

Posted on
Thu Sep 09, 2021 5:07 pm
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Re: Looking for a mentor to help me build a new plugin

Thanks. I've made that change and I'll be testing it tonight. In the meantime, here's what's returned by the API. I've changed the values for privacy. To answer your question, I'd like to start with just 1 account, 1 circle being supported. So 1 account will allow devices to be created for every circle member that is returned. My MVP1 is to have device states for each user (device) to support:

- First Name
- Last Name
- Email Address
- Phone Number
- Is battery charging
- Battery Level
- Lat
- Long
- Circle Name (this is the name of the geofence this user is in, defined on the app) Empty if not in a fence
- Closest Address to user (I'm using geopy for this by sending lat long and getting an address as a return


Code: Select all

{
  u'name': u'Family',
  u'color': u'7f26c2',
  u'memberCount': u'3',
  u'members': [
    {
      u'pinNumber': None,
      u'features': {
        u'smartphone': u'1',
        u'disconnected': u'0',
        u'geofencing': u'1',
        u'mapDisplay': u'1',
        u'shareLocation': u'1',
        u'device': u'1',
        u'shareOffTimestamp': None,
        u'nonSmartphoneLocating': u'0',
        u'pendingInvite': u'0'
      },
      u'firstName': u'Ryan',
      u'lastName': u'Buckner',
      u'medical': None,
      u'loginPhone': u'+17035551212',
      u'createdAt': u'1629681164',
      u'loginEmail': u'ryanbuckner@gmail.com',
      u'communications': [
        {
          u'type': u'Home',
          u'value': u'+17035551212',
          u'channel': u'Voice'
        },
        {
          u'type': None,
          u'value': u'ryanbuckner@gmail.com',
          u'channel': u'Email'
        }
      ],
      u'isAdmin': u'0',
      u'relation': None,
      u'location': {
        u'isDriving': u'0',
        u'shortAddress': u'Mains St Drive',
        u'battery': u'31',
        u'startTimestamp': 1631219160,
        u'speed': -1,
        u'inTransit': u'0',
        u'tripId': None,
        u'since': 1631219160,
        u'driveSDKStatus': None,
        u'source': u'l',
        u'charge': u'0',
        u'wifiState': u'1',
        u'address1': u'Main St  Drive',
        u'latitude': u'39.003249',
        u'endTimestamp': u'1632427489',
        u'accuracy': u'65',
        u'userActivity': None,
        u'timestamp': u'1631227489',
        u'address2': u'Virginia',
        u'name': u'Home',
        u'sourceId': u'5a13aa2a-2ada-4595-8acb-34ca6465f1aa',
        u'longitude': u'-77.372384',
        u'placeType': None
      },
      u'activity': None,
      u'id': u'd88322d1-f894-4dca-89b0-9ce0b9eb3281',
      u'issues': {
        u'status': None,
        u'disconnected': u'0',
        u'title': None,
        u'troubleshooting': u'0',
        u'dialog': None,
        u'action': None,
        u'type': None
      },
      u'avatar': u'https://www.life360.com/img/user_images/d88322d1-f894-4dca-89b0-9ce0b9eb3281/df35acf2-bd16-4b05-9f73-02cfce6bcb2b.jpg?fd=2'
    },
    {
      u'pinNumber': None,
      u'features': {
        u'smartphone': u'1',
        u'disconnected': u'0',
        u'geofencing': u'1',
        u'mapDisplay': u'1',
        u'shareLocation': u'1',
        u'device': u'1',
        u'shareOffTimestamp': None,
        u'nonSmartphoneLocating': u'0',
        u'pendingInvite': u'0'
      },
      u'firstName': u'user 2',
      u'lastName': u'buckner',
      u'medical': None,
      u'loginPhone': u'+15715551212',
      u'createdAt': u'1629680785',
      u'loginEmail': u'example@gmail.com',
      u'communications': [
        {
          u'type': u'Home',
          u'value': u'+15715551212',
          u'channel': u'Voice'
        },
        {
          u'type': None,
          u'value': u'example@gmail.com',
          u'channel': u'Email'
        }
      ],
      u'isAdmin': u'0',
      u'relation': None,
      u'location': {
        u'isDriving': u'0',
        u'shortAddress': u'Main St Drive',
        u'battery': u'4',
        u'startTimestamp': 1631213169,
        u'speed': -1,
        u'inTransit': u'0',
        u'tripId': None,
        u'since': 1631213169,
        u'driveSDKStatus': None,
        u'source': u'l',
        u'charge': u'0',
        u'wifiState': u'1',
        u'address1': u'Main St Drive',
        u'latitude': u'39.033961',
        u'endTimestamp': u'1631227428',
        u'accuracy': u'65',
        u'userActivity': None,
        u'timestamp': u'1631227428',
        u'address2': u'Virginia',
        u'name': u'Home',
        u'sourceId': u'5a13aa2a-2ada-4595-8acb-34ca6465f1aa',
        u'longitude': u'-77.336934',
        u'placeType': None
      },
      u'activity': None,
      u'id': u'725e209b-05e4-4b32-a66d-05185090056a',
      u'issues': {
        u'status': None,
        u'disconnected': u'0',
        u'title': None,
        u'troubleshooting': u'0',
        u'dialog': None,
        u'action': None,
        u'type': None
      },
      u'avatar': u'https://www.life360.com/img/user_images/725e209b-05e4-4b32-a66d-05185090056a/ba8cd743-49e2-4afe-849b-d24545da377d.jpg?fd=2'
    },
    {
      u'pinNumber': None,
      u'features': {
        u'smartphone': u'1',
        u'disconnected': u'0',
        u'geofencing': u'1',
        u'mapDisplay': u'1',
        u'shareLocation': u'1',
        u'device': u'1',
        u'shareOffTimestamp': None,
        u'nonSmartphoneLocating': u'0',
        u'pendingInvite': u'0'
      },
      u'firstName': u'User 3',
      u'lastName': u'',
      u'medical': None,
      u'loginPhone': u'+17035551222',
      u'createdAt': u'1564837778',
      u'loginEmail': u'example@gmail.com',
      u'communications': [
        {
          u'type': u'Home',
          u'value': u'+17035551212',
          u'channel': u'Voice'
        },
        {
          u'type': None,
          u'value': u'dexample@gmail.com',
          u'channel': u'Email'
        }
      ],
      u'isAdmin': u'1',
      u'relation': None,
      u'location': {
        u'isDriving': u'0',
        u'shortAddress': u'Main St Drive',
        u'battery': u'100',
        u'startTimestamp': 1630431355,
        u'speed': 0,
        u'inTransit': u'0',
        u'tripId': None,
        u'since': 1630431355,
        u'driveSDKStatus': None,
        u'source': u'l',
        u'charge': u'1',
        u'wifiState': u'1',
        u'address1': u'Landerset Drive',
        u'latitude': u'39.0078378',
        u'endTimestamp': u'1631226753',
        u'accuracy': u'50',
        u'userActivity': None,
        u'timestamp': u'1631226753',
        u'address2': u'Virginia',
        u'name': u'Home',
        u'sourceId': u'5a13aa2a-2ada-4595-8acb-34ca6465f1aa',
        u'longitude': u'-77.344751',
        u'placeType': None
      },
      u'activity': None,
      u'id': u'97ded1d4-db31-403b-a9e2-63477a9f9f6b',
      u'issues': {
        u'status': None,
        u'disconnected': u'0',
        u'title': None,
        u'troubleshooting': u'0',
        u'dialog': None,
        u'action': None,
        u'type': None
      },
      u'avatar': u'https://www.life360.com/img/user_images/97ded1d4-db31-403b-a9e2-63477a9f9f6b/6f96e91b-f8a4-4992-af75-ea215db4fe0e.jpg?fd=2'
    }
  ],
  u'unreadNotifications': u'0',
  u'unreadMessages': u'0',
  u'type': u'basic',
  u'id': u'd841bd4f-3bb1-46f7-b25f-810635e3d2af',
  u'createdAt': u'1564837778',
  u'features': {
    u'skuId': 8,
    u'premium': u'1',
    u'priceYear': u'0',
    u'skuTier': 4,
    u'locationUpdatesLeft': 0,
    u'priceMonth': u'0',
    u'ownerId': u'97ded1d4-db31-403b-a9e2-63477a9f9f6b'
  }
}


Here's the script I'm basing this plugin from. which I'm sure will help.

Code: Select all
from life360 import life360
import datetime
import indigo
from geopy.geocoders import Nominatim
from config import authorization_token, password, username #deprecated

plugin_name = "Life 360 Plugin"
#indigo.server.log(u"Starting script...", type=plugin_name)

if __name__ == "__main__":
    try:
        # instantiate the API
        api = life360(authorization_token=authorization_token, username=username, password=password)
        if api.authenticate():
            circles = api.get_circles()
            id = circles[0]['id']
            circle = api.get_circle(id)

            #instantiate the geocorder to reverse lat long into an address
            geocoder = Nominatim(user_agent='life360')

            # grab the information from the life360 json response
            for m in circle['members']:
                bat = int(float(m['location']['battery']))
                loclat = float(m['location']['latitude'])
                loclng = float(m['location']['longitude'])
                location = m['location']['name']
                phone_num = m['communications'][0]['value']
                memberid = m['id']
                first_name = m['firstName']
                charging = m['location']['charge']
                avatar = m['avatar']

                try:
                    # get address from lat long information
                    geoloc = geocoder.reverse((loclat, loclng))
                    currentaddress = geoloc
                except GeocoderTimedOut as g:
                    indigo.server.log(u"Geocoder timed out: " + g.msg, type=plugin_name)
                    currentaddress = "unknown - geocoder error"

                # assign user specific informaton to Indigo variables
                if memberid == 'd88322d1-f894-4dca-89b0-9ce0b9eb3281': #Ryan
                    print("This is Ryan - " + first_name)
                elif memberid == '97ded1d4-db31-403b-a9e2-63477a9f9f6b': #Wife
                    #print("This is user 2  - " + first_name)
                    indigo.variable.updateValue(80336455, value=location)
                    indigo.variable.updateValue(1958153929, value=str(bat))
                    indigo.variable.updateValue(319900190, value=str(loclat))
                    indigo.variable.updateValue(345547923, value=str(loclng))
                    indigo.variable.updateValue(1984283932, value=str(currentaddress))
                    indigo.variable.updateValue(482459252, value=str(charging))
                    indigo.variable.updateValue(1836853387, value=str(avatar))
                elif memberid == '725e209b-05e4-4b32-a66d-05185090056a': # User 3
                    indigo.variable.updateValue(771386486, value=location)
                    indigo.variable.updateValue(186608979, value=str(bat))
                    indigo.variable.updateValue(915555447, value=str(loclat))
                    indigo.variable.updateValue(1655256712, value=str(loclng))
                    indigo.variable.updateValue(836244599, value=str(currentaddress))
                    indigo.variable.updateValue(44126099, value=str(charging))
                    indigo.variable.updateValue(1909192424, value=str(avatar))
        else:
            print("Error authenticating life360 api. Check your username and password")
    except Exception as e:
        indigo.server.log(e, "life360 script")
    else:
        pass
        #indigo.server.log("life 360 process successful", plugin_name)


Posted on
Thu Sep 09, 2021 5:34 pm
ryanbuckner offline
Posts: 1080
Joined: Oct 08, 2011
Location: Northern Virginia

Re: Looking for a mentor to help me build a new plugin

neilk wrote:
OK - to help me get my head around the device updates could you post what the API returns and we can figure out how to split this out by device. That may involve shifting to add in the update mechanisms from another plugin.

In the mean time we can look to use the plugin config details rather than the config file. I don't know anything about life360, but you have a choice to use one set of login credential globally for the plugin and have devices created per family member or have the device associated with a separate account and family member. It depends if you would ever need to access more than one account.

That will could how we implement it but given you have added the config items you can use

Code: Select all
self.pluginPrefs['life360_username']
in your api call (and the same for token and password) and then you can remove the config file


Ok, plugin config credentials successfully used! I'll deprecate the config.py

- I would love to add a button to the config screen to verify testing the credentials. But that's lower priority

Posted on
Fri Sep 10, 2021 4:52 am
DaveL17 offline
User avatar
Posts: 6753
Joined: Aug 20, 2013
Location: Chicago, IL, USA

Re: Looking for a mentor to help me build a new plugin

I would love to add a button to the config screen to verify testing the credentials. But that's lower priority

Nothing to do with priority, but unless you truly want the user to have an interactive confirmation that the credentials are valid, you can test their validity using the appropriate validateConfigUi() callback and alert the user if the credentials fail validation.

I came here to drink milk and kick ass....and I've just finished my milk.

[My Plugins] - [My Forums]

Posted on
Fri Sep 10, 2021 10:48 am
neilk offline
Posts: 715
Joined: Jul 13, 2015
Location: Reading, UK

Re: Looking for a mentor to help me build a new plugin

DaveL17 wrote:
I would love to add a button to the config screen to verify testing the credentials. But that's lower priority

Nothing to do with priority, but unless you truly want the user to have an interactive confirmation that the credentials are valid, you can test their validity using the appropriate validateConfigUi() callback and alert the user if the credentials fail validation.


So I use the approach that Dave refers to in my GlowMarkt plugin https://github.com/neilkplugins/Glow-IHD-CAD-indigo-plugin/blob/master/GlowmarktCAD.indigoPlugin/Contents/Server%20Plugin/plugin.py, rather than having a validation button you simply cannot close/leave the dialogue without having valid credentials, and you can alert the user. Based on the code you have and the example below I think you can get to close to what you want (this validation only applies if the "API enable" check box is set, so you can remove that first if)

Code: Select all
def validatePrefsConfigUi(self, valuesDict):
      if valuesDict['API_enable'] is True:
         if not (valuesDict['bright_account']):
            self.errorLog("Account Email Cannot Be Empty")
            errorsDict = indigo.Dict()
            errorsDict['bright_account'] = "Glow/Bright Account Cannot Be Empty"
            return (False, valuesDict, errorsDict)
         if not (valuesDict['bright_password']):
            self.errorLog("Password Cannot Be Empty")
            errorsDict = indigo.Dict()
            errorsDict['bright_password'] = "Password Cannot Be Empty"
            return (False, valuesDict, errorsDict)
      try:
         url = "https://api.glowmarkt.com/api/v0-1/auth"

         payload = "{\n\"username\": \"" + valuesDict['bright_account'] + "\",\n\"password\": \"" + valuesDict[
            'bright_password'] + "\"\n}"
         headers = {
            'Content-Type': 'application/json',
            'applicationId': "b0f1b774-a586-4f72-9edd-27ead8aa7a8d",
            'Content-Type': 'application/json'
         }
         try:
            response = requests.request("POST", url, headers=headers, data=payload)
            response.raise_for_status()
         except requests.exceptions.HTTPError as err:
            self.debugLog("HTTP Error when authenticating to Glowmarkt")
         except Exception as err:
            self.debugLog("Other error when authenticating to Glowmarkt")
         self.debugLog(response)
         response_json = response.json()
         self.debugLog(response_json)
         self.debugLog(response_json['token'])
         if response.status_code != 200:
            self.errorLog("Failed to Authenticate with Glow Servers, Check Password and Account Name")
            errorsDict = indigo.Dict()
            errorsDict[
               'bright_password'] = "Failed to Authenticate with Glow Servers, Check Password and Account Name"
            return (False, valuesDict, errorsDict)
         else:
            self.pluginPrefs['token'] = response_json['token']
            self.pluginPrefs['token_expires'] = response_json['exp']
            self.debugLog("Token is " + self.pluginPrefs['token'])
            self.debugLog("Expiry is " + str(self.pluginPrefs['token_expires']))
            get_resources(self)

      except:
         self.debugLog("Unknown error connecting to Glowmarkt")
         errorsDict = indigo.Dict()
         errorsDict['bright_password'] = "Failed to Authenticate with Glow Servers, Check Password and Account Name"
         return (False, valuesDict, errorsDict)

      return (True, valuesDict)


With more explanation of how to do this https://wiki.indigodomo.com/doku.php?id=indigo_6_documentation:plugin_guide#validation_methods

As long as you do not have really tight limitations on the number or API calls (or account lockouts on password fails) you can test this approach and mess up the credentials.

I will respond separately with ideas on the main update cycle and approach for the device type

Posted on
Fri Sep 10, 2021 11:52 am
neilk offline
Posts: 715
Joined: Jul 13, 2015
Location: Reading, UK

Re: Looking for a mentor to help me build a new plugin

ryanbuckner wrote:
Thanks. I've made that change and I'll be testing it tonight. In the meantime, here's what's returned by the API. I've changed the values for privacy. To answer your question, I'd like to start with just 1 account, 1 circle being supported. So 1 account will allow devices to be created for every circle member that is returned. My MVP1 is to have device states for each user (device) to support:

- First Name
- Last Name
- Email Address
- Phone Number
- Is battery charging
- Battery Level
- Lat
- Long
- Circle Name (this is the name of the geofence this user is in, defined on the app) Empty if not in a fence
- Closest Address to user (I'm using geopy for this by sending lat long and getting an address as a return



So the approach I would suggest is have the API call made once and store the resultant JSON, and then have each device update method update the device states by member. You will see and example of a partial device definition below that defines a resource type by a list generator method. I suspect your "get_member_list" will already return a list of first names, for which you could then store as a "member_name" field for that device.



Code: Select all
   <Device type="custom" id="daily_Consumption">
         <Name>Glowmarkt Daily Consumption </Name>
      <ConfigUI>
            <Field id="resource_type" type="menu">
                <Label>Resource Type-</Label>
                <List class="self" filter="" method="resourceListGenerator" />
            </Field>
<Field id="dayList" type="menu">
<Label>Choose:</Label>
<List>
   <Option value="today">For Todays Rates (requires CAD Device others updates will be no more than every 30 mins)</Option>
   <Option value="yesterday">Yesterdays Rates</Option>
</List>
</Field>
         <Field id="simpleseparator1" type="separator">
   </Field>
<Field id="Pound_enable" type="checkbox" defaultValue="false" >
      <Label>Present Daily Usage Data in £ vs pence</Label>
   </Field>
   <Field id="Label2" type="label" fontSize="small" fontColor="darkgray"><Label>If checked the device state will be shown in £, unchecked in pence to 2 decimal places for daily consumption devices</Label>
   </Field>
        </ConfigUI>


The main update logic will then iterate through all of your devices, use the stored JSON to update just the device states for that member, then do the next. I will dig out examples to point you in the right direction but it may be tomorrow.

Who is online

Users browsing this forum: No registered users and 5 guests

cron