itchannel / apex-ha

Local Neptune Apex HA Integration (Aquarium Controller)
GNU General Public License v3.0
19 stars 3 forks source link

Discussion: Controlling DOS Pumps #29

Open brettonw opened 1 year ago

brettonw commented 1 year ago

I have been trying to figure out how to control my DOS pump through the HA integration, and I've arrived at an intermediate hack solution that works for now, but I wonder if there's a better way.

You have two choices with the DOS. You can set it to OFF/ON, which will give you 0 or 25ml/min dosing rates. Or you can set a profile.

Unfortunately the Neptune profiles are over-constrained, so while there is some ability to set the DOS pump speed, you can't just set it to that speed and let it go. Apex will always try to manage a dose, instead of a rate. The actual dose appears to be limited to 1/3rd of the rate. I don't understand what the reasoning behind this might be. I'm curious if it's enforced at the Apex, or just in the ApexFusion UI.

In order to try to get a controlled rate, I meticulously went into ApexFusion and created 10 profiles corresponding to 1ml/min up to 10ml//min.

Screen Shot 2022-09-11 at 11 50 33 AM Screen Shot 2022-09-11 at 11 50 16 AM

I created an input_number with a range from 0..10, and I update the profile on the Apex device with an automation like this:

input_number:
  dos_rate:
    name: "DOS rate"
    min: 0
    max: 10
    step: 1
    icon: mdi:speedometer

automation:
  - id: update_dos_rate
    alias: Update DOS Rate
    trigger:
      - platform: state
        entity_id: input_number.dos_rate
    action:
      - service: apex.set_variable
        data:
          did: "4_1"
          code: >
            {% if (trigger.to_state.state | int) == 0 %} 
            Set OFF 
            {% else %} 
            Set Dose{{ (trigger.to_state.state | int) }} 
            {% endif %}
    mode: single

I expose this on lovelace, so I have a slider:

Screen Shot 2022-09-11 at 11 53 20 AM
brettonw commented 1 year ago

So I wonder if there's a better way. If it's possible to just set a single profile, and update the profile parameters with different pump speeds. If the constraints on dosing are only enforced in the Apex Fusion UI, it might be possible to just set the pump speed in the profile and let it run. Is it worth a try?

brettonw commented 1 year ago

A response in the neptune forums indicated the reason for the 1/3rd rate restraint is the pumps are not rated for continuous use, so they don't want to shorten the pump's life. I read somewhere in Neptune's literature the DOS pumps are designed for 5,000 hours of operation.

I still think updating the profile might be a more useful approach to controlling the DOS than just having a bunch of presets and a slider. I have to set up the presets at the resolution I might want, and Neptune only allows 32 profiles in total across all devices. If I wanted to run 4 different DOS pumps with different possible ranges, I'm out of profiles...

brettonw commented 1 year ago

URL: http://apex.local/rest/config/pconf/1 Payload:

{
    "name": "Dose1",
    "ID": 1,
    "type": "dose",
    "data": {
        "mode": 21,
        "count": 255,
        "time": 60,
        "amount": 1
    }
}

mode is set from a composite value, based on the setting pair for rate and direction. bits 0-4 set the speed:

      return ["250 mL / min", "125 mL / min", "60 mL / min", "25 mL / min", "12 mL / min", "7 mL / min"][-17 & t]

(Programmer geekery -> what's wrong with using 15 instead of -17 in this case?)

bit 5 sets the direction:

      return ["Reverse", "Forward"][16 & t ? 1 : 0]

so mode 21 is speed 5 ("7 mL / min") + 16 ("Forward").

brettonw commented 1 year ago

I added a service call 'set_dos_rate'

set_dos_rate:
  description: "Set the dosing rate for a DOS pump"
  fields:
    did:
      name: DID
      description: "DID of the DOS pump"
      example: "4_1"
      selector:
        text:
    profile_id:
      name: Profile ID
      description: "Profile ID to assign to the DOS pump, the integration will rename it appropriately."
      example: 11
      selector:
        text:
    rate:
      name: Rate
      description: "The desired dosing rate, in mL / min. (effective range is 0 - 125)"
      example: 1.2
      selector:
        text:

The code behind the service is:

    def set_dos_rate(self, did, profile_id, rate):
        headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
        config = self.config()

        profile = config["pconf"][profile_id - 1]
        if int(profile["ID"]) != profile_id:
            return {"error": "Profile index mismatch"}

        # turn the pump off to start - this will enable a new profile setting to start immediately
        # without it, the DOS will wait until the current profile period expires
        off = self.set_variable(did, f"Set OFF")
        if off["error"] != "":
            return off

        # check if the requested rate is greater than the OFF threshold
        min_rate = 0.1
        if rate > min_rate:
            # our input is a target rate (ml/min). we want to map this to the nearest 0.1ml/min, and
            # then find the slowest pump speed possible to manage sound levels. Neptune uses a 3x
            # safety margin to extend the life of the pump, but the setting only appears to be
            # enforced in the Fusion UI. We use a 2x margin because we can.
            pump_speeds = [250, 125, 60, 25, 12, 7]
            safety_margin = 2
            rate = int(rate * 10) / 10.0
            if int(pump_speeds[0] / safety_margin) >= rate:
                target_pump_speed = rate * safety_margin
                pump_speed_index = len(pump_speeds) - 1
                while pump_speeds[pump_speed_index] < target_pump_speed:
                    pump_speed_index -= 1

                # bits 0-4 of the 'mode' value are the pump speed index, and bit 5 specifies
                # 'forward' or 'reverse'. we always use 'forward' because you can't calibrate the
                # reverse direction using the Apex dashboard
                mode = pump_speed_index + 16

                # we set the profile to be what we need it to be so the user doesn't have to do
                # anything except choose the profile to use
                profile["type"] = "dose"
                profile["name"] = f"Dose_{did}"

                # the DOS profile is the mode, target amount, target time period (one minute), and
                # dose count
                profile["data"] = {"mode": mode, "amount": rate, "time": 60, "count": 255}
                _LOGGER.debug(profile)

                r = requests.put(f"http://{self.deviceip}/rest/config/pconf/{profile_id}", headers=headers, json=profile)
                # _LOGGER.debug(r.text)

                # turn the pump on
                return self.set_variable(did, f"Set {profile['name']}")
            else:
                return {"error": f"Requested rate ({rate} mL / min) exceeds the supported range (limit {int(pump_speeds[0] / safety_margin)} mL / min)."}
        else:
            # XXX TODO handle 0 < rate < 0.1ml/min by dosing over multiple minutes? Is this necessary?
            return {"error": ""}

And my input_number setup to use it:

input_number:
  dos_rate:
    name: "DOS rate"
    min: 0
    max: 5
    step: 0.1
    icon: mdi:speedometer

automation:
  - id: update_dos_rate
    alias: Update DOS Rate
    trigger:
      - platform: state
        entity_id: input_number.dos_rate
    action:
      - service: apex.set_dos_rate
        data:
          did: "4_1"
          profile_id: 11
          rate: >
            {{ (trigger.to_state.state | float) }}
    mode: single
brettonw commented 1 year ago

The DOS sensor returns the amount of volume remaining in the reservoir, but could also return the capacity from the status.

The Neptune module has a setting for the reservoir capacity, and there is a place you can go in Fusion to "fill" the reservoir. The Apex automatically subtracts the volume delivered by the DOS from the remaining volume. You can also configure an optical water level sensor or the Neptune DDR to automatically set these values. For our purposes, these values can also be read and set using the mconf endpoint. Note this applies to both DOS pumps in a DOS module.

URL: http://apex.local/rest/config/mconf/4 Payload:

{
    "abaddr": 4,
    "name": "DOS_4",
    "hwtype": "DOS",
    "update": false,
    "updateStat": 0,
    "extra": {
        "volume": [
            20000,
            2000
        ],
        "volumeLeft": [
            20000,
            0
        ],
        "swapAddr": 0
    },
    "action": 0,
    "errorCode": 0,
    "errorMessage": ""
}

I've got my DOS connected to a 5 gallon jug next to my tank as a top-off reservoir (for now), so I don't see using the sensor approach (for now). Maybe the DOS sensor exposed to HA should be a special sensor that has both the capacity and the remaining volume, and exposes a "refill" button, or a "refill" service that takes an amount as a parameter?

brettonw commented 1 year ago

Just an update that I've implemented this in my own fork, and have given up using water level controls in my ATO. I'm using the salinity (filtered signal) as a direct control with a proportional response. It's working pretty well so far.

https://github.com/brettonw/apex-ha

Screen Shot 2022-10-03 at 12 31 13 PM
brettonw commented 1 year ago

I also added a DOS refill service to my fork at https://github.com/brettonw/apex-ha

Screen Shot 2022-10-22 at 3 05 39 PM