Creating a new driver for PetSafe Smart Feeder v2

Hello, after days of work I customized existing github apis to create an edge driver for my smart feeder.
I managed to install the driver on my hub, but now I am stuck as I do not know how to add a new device that uses the driver.
The code I used is very simple, it is in python, but it works and the food calculations are accurate.
I just need help to figure out how to add the device, I am probably skipping a step?

Ok, I read this: FAQ, that helped, I reckon I cannot find the petfeeder, even if it is connected and working and it responds to the API because my permissions file is wrong:

name: "PetSafe Smart Feeder"
packageKey: "petsafe-feeder"
permissions:
   discovery: {}

I added the lan: {} permission, but still no luck in finding the device, what am I doing wrong?

What ST hub do you have and how did you think you installed a Python-based driver on it? ST drivers are written in Lua and support hub devices that use Z-Wave, Zigbee, Matter, and LAN Websocket protocols.

Is this the existing Github APIs you are referring to? GitHub - Techzune/petsafe_smartfeed: Connect and control a PetSafe Smart Feed device using the PetSafe-SmartFeed API.

If so, this is Python code to call the vendor’s APIs to manage the pet feeder over the Internet. You will not be able to call those APIs from a ST hub which is limited to communicating on the local LAN. You could use a 3rd party driver that allows you to make HTTP/S calls, but requires a proxy to communicate from the hub to the Internet. It doesn’t appear that the device supports Websockets (at least that I can see) so you won’t be able to discover it on the local LAN.

This is the ST architecture:

The proper implemenation for this device is a cloud->cloud integration which would need to be developed by the vendor. You might be able to develop a Smartapp that you host on your own server somewhere, but I don’t have experience in that area.

3 Likes

Hi! Thanks so much for getting back to me, apols if I was unclear.
Yes, those are the APIs I used!
These are the steps I followed:

  1. Test standard API calls to the device
  2. Customise the .py script to automatically fetch auth tokens and calculate remaining food
  3. Create a Flask container on my local server to interact with the device, when I call the server I get the response from the API
  4. Create the .lua driver based on the functionalities and code of the py scripts

I know I did not and cannot install py files on the hub.

What I reckon is that I did not test the lua file, so probably this is why I cannot find the device, even if the driver is installed on the hub.

I would however really appreciate any help if I can share here the code for review.

Would you mind to paste the lua file here?

Just make sure to add ``` before and after the code to make it more readable. Like this:

```
type or paste code here
```

So it shows up like this:

type or paste code here
1 Like

I have finally found the full guide on how to develop a driver, it is quite difficult, but not impossible, I suppose, unless the lua code below is very close ti being right, I have a lot of testing work to do.

local capabilities = require('st.capabilities')
local Driver = require('st.driver')
local log = require('st.log')
local json = require('dkjson')
local cosock = require('cosock')
local http = cosock.asyncify('socket.http')
local ltn12 = require('ltn12')

-- Device configuration
local REFRESH_INTERVAL = 300 -- 5 minutes

-- Bridge communication functions
local function build_bridge_url(device)
    local addr = device.preferences.bridgeAddress or 'localhost'
    local port = device.preferences.bridgePort or 5000
    return string.format('http://%s:%s', addr, port)
end

local function make_request(device, path, method)
    local url = build_bridge_url(device) .. path
    local response_body = {}
    
    local success, code = http.request{
        url = url,
        method = method or 'GET',
        sink = ltn12.sink.table(response_body),
        headers = {
            ['Content-Type'] = 'application/json',
        }
    }
    
    if success and code == 200 then
        local body = table.concat(response_body)
        local decoded = json.decode(body)
        return decoded
    else
        log.error(string.format('[%s] Request failed: %s, code: %s', device.label, url, code))
        return nil
    end
end

-- Device status update
local function update_device_status(device, status)
    if status.battery then
        -- Convert voltage to percentage (assuming 6V max, 3.5V min)
        local battery_pct = math.floor(((status.battery - 3.5) / 2.5) * 100)
        battery_pct = math.max(0, math.min(100, battery_pct))
        device:emit_event(capabilities.battery.battery(battery_pct))
    end
    
    if status.connected ~= nil then
        if status.connected then
            device:online()
        else
            device:offline()
        end
    end
    
    if status.food_low ~= nil then
        device:emit_event(capabilities.switch.switch.off())
    end
end

-- Capability handlers
local function handle_refresh(driver, device)
    log.info(string.format('[%s] Refreshing device status', device.label))
    
    local status = make_request(device, '/status/' .. device.device_network_id)
    if status then
        update_device_status(device, status)
    end
end

local function handle_feed(driver, device)
    log.info(string.format('[%s] Manual feed triggered', device.label))
    
    -- Get portion size from preferences
    local portions = device.preferences.portionSize or 1
    
    local result = make_request(
        device, 
        string.format('/feed/%s?portions=%d', device.device_network_id, portions),
        'POST'
    )
    
    if result and result.status == 'success' then
        -- Emit temporary "on" state
        device:emit_event(capabilities.switch.switch.on())
        
        -- Schedule switch to turn off after 2 seconds
        device.thread:call_with_delay(2, function()
            device:emit_event(capabilities.switch.switch.off())
        end)
        
        -- Refresh status after feeding
        device.thread:call_with_delay(5, function()
            handle_refresh(driver, device)
        end)
    end
end

-- Lifecycle handlers
local function device_init(driver, device)
    log.info(string.format('[%s] Initializing device', device.label))
    
    -- Set initial state
    device:emit_event(capabilities.switch.switch.off())
    
    -- Initial refresh
    handle_refresh(driver, device)
    
    -- Schedule regular updates
    device.thread:call_on_schedule(
        REFRESH_INTERVAL,
        function()
            handle_refresh(driver, device)
        end
    )
end

local function device_added(driver, device)
    log.info(string.format('[%s] Adding device', device.label))
    device_init(driver, device)
end

local function device_doconfigure(driver, device)
    log.info(string.format('[%s] Configuring device', device.label))
    -- Validate bridge connection
    local status = make_request(device, '/status/' .. device.device_network_id)
    if status then
        device:online()
        return true
    end
    return false
end

-- Driver definition
local driver = Driver('PetSafe-Feeder', {
    discovery = nil,  -- Manual device addition only
    lifecycle_handlers = {
        init = device_init,
        added = device_added,
        doConfigure = device_doconfigure,
    },
    capability_handlers = {
        [capabilities.refresh.ID] = {
            [capabilities.refresh.commands.refresh.NAME] = handle_refresh,
        },
        [capabilities.switch.ID] = {
            [capabilities.switch.commands.on.NAME] = handle_feed,
        },
    },
})

log.info('Starting PetSafe Feeder driver')
driver:run()

Alright. From a first (and probably last for tonight) glance, you want to use a proxy or bridge as you call it.

localhost and everything outside the LAN won’t work, as @h0ckeysk8er already mentioned.

What you need is a discovery_handler, as shown here (line 655):

This driver shows how to create a basic LAN device that can communicate with your proxy. It will be discovered when you “scan for devices” from within the SmartThings app.

1 Like

Thank you! I realised it is too much for me, I will just create a webserver, put it behind the existing infrastructure and build a simple webpage to achieve most of the results, bar the ST integration.
I created all this with zero coding knowledge, just using LLMs and I am very happy for what I managed to do in just a few days…but there is prob a limit to what can be done without experience

LLMs won’t help you and you should have mentioned that from the start. Otherwise you’re wasting our time.

That’s what I thought when I saw your code: it looks a lot like a working driver but not really. Uncanny valley…

Here’s a code snippet from one of my drivers, a very simple virtual temperature sensor:

local Driver = require "st.driver"
local log = require "log"
local capabilities = require "st.capabilities"
local VirtualTemperature = capabilities["oceancircle09600.vtempset"]
local IS_INITIALIZED = "is_initialized"

local function discovery_handler(self, opts)
  log.debug("HANDLE_DISCOVERY")
  if not self.datastore[IS_INITIALIZED] then
    local device_data = {
      type = "LAN",
      device_network_id = "virtual-temperature",
      label = "Virtual Temperature",
      profile = "virtual-temperature",
      manufacturer = "Virtual Manufacturer",
      model = "Virtual Model",
      vendor_provided_label = "Virtual Temperature"
    }
    self:try_create_device(device_data)
  end
end

Typical discovery_handler for a virtual device.

Good luck!

1 Like

Hi Andreas, I do not feel the same, LLM are a massive help in order to bring people closer to the code and the community.
Being a community, I take it that every contribution help, without LLM even using the APIs would have been significantly harder for me, let alone the very complex archoitecture of a ST integraton. The limit of the LLM is the limited amount of codebase for ST integrations it can learn from.

I have to agree that trying to use that code as starting point is wasting time, there are so many hallucinations, API endpoints that do not exist, packages that do not exist, code that is useless and an overall wrong strategy using the switch on/off capability as way to trigger the feed.

The whole approach of the AI that generated it is behaving as a virtual switch that when you turn it on it generates the request to feed and then turns it off. You can already do that with the WebRequestor linked before (and a local proxy). Or with a virtual switch and an automation engine like SharpTools to make the request, although that’s a paid feature.

Now, I would not trust it to feed a living thing since there’s basically no feedback about the success of the operation. Maybe there was a network issue or a cloud issue, you’ll never know.

Also, as pointed out, since drivers can’t connect to the Internet it is not the best approach to integrate this device.

1 Like

THis is fine and I fully agree with you, I could not possibly know that since the LLM provided excellent inout in creatin the pythn files based on the existing code, I will scrap the project as it’s just too dififcult for me; if I go back at it, I will use the provided examples and the documentatin I found to point the LLM in the right direction. And I will do some tests on my own.

Are you interested in the integration for any particular purpose? I don’t know, schedule the feeds, getting a low battery notification, having widgets or whatnot.

Since they are just HTTP requests you can also use Android automation apps like Tasker or Macrodroid.

1 Like

Thank you for following up, there are multiple reasons why I started with this integration, the main one is that, having the feeder added to the ST ecosystem makes it easier to integrate with other routines, also calculating the amount of food left accurately is useful, and it is a nice exercise . But I see it is just too much effort

There are pet feeders compatible with SmartThings, like the Aqara Smart Pet Feeder C1.

Regarding LLMs: they are very useful for learning how ST Edge Drivers work. You could paste the init.lua of an existing, working driver and ask questions like “How does this work?”, “What is a handler?”, “What is needed to add humidity support?” and so on. It’s pretty much useless for now to create a driver from scratch - but that’s just a question of time…

Slightly off-topic: it’s also extremely useful for creating a README.md for GitHub form an init.lua alone! Everyone should try it, it’s awesome! :wink:

That’s one of those cases where an automation app comes in handy, with Tasker or Macrodroid you can periodically retrieve the JSON, check the food_sensor_xxx attributes and send you notifications for instance. Back when PATs in SmartThings lasted more than 24 hours you could also run SmartThings routines, not viable anymore.

Anyway, you’ll find this thread in Home Assistant useful too. After all, with SmartThings you would need another machine running a proxy, you could as well run HA in that machine.