diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7f2b2257bf2f3f4c8c43f6edf8e91d80345cc999..ff459e83dfb1382b260b44d514ac7d2c2bbb07ed 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,7 +4,7 @@ stages:
   - deploy
 
 image:
-  name: esphome/esphome:2023.9.3
+  name: esphome/esphome:2023.10.6
   entrypoint: [""]
 
 validate_configs:
diff --git a/growbox.yaml b/growbox.yaml
index be6d0211961e5646bb4e4617209a1bcd1626720e..6661bdeb7500921a5d9081193ad6a496b7fac1aa 100644
--- a/growbox.yaml
+++ b/growbox.yaml
@@ -27,3 +27,4 @@ packages:
   ec:             !include packages/ec.yaml
   ph:             !include packages/ph.yaml
   sts35:          !include packages/sts35.yaml
+  pca9685:        !include packages/pca9685.yaml
diff --git a/packages/pca9685.yaml b/packages/pca9685.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..08df1a32893b1f0c6bbfa0675d726fe687470b43
--- /dev/null
+++ b/packages/pca9685.yaml
@@ -0,0 +1,61 @@
+globals:
+  - id: pump1_ml_sec
+    type: float
+    initial_value: !secret pump1_ml_sec
+  - id: pump1_dose_ml
+    type: int
+    initial_value: !secret pump1_dose_ml
+  - id: pump2_ml_sec
+    type: float
+    initial_value: !secret pump2_ml_sec
+  - id: pump2_dose_ml
+    type: int
+    initial_value: !secret pump2_dose_ml
+
+pca9685:
+  - id: pca9685_dosing
+    frequency: 100Hz
+
+output:
+  - platform: pca9685
+    id: output_ph_down
+    pca9685_id: pca9685_dosing
+    channel: 8
+  - platform: pca9685
+    id: output_ph_up
+    pca9685_id: pca9685_dosing
+    channel: 7
+    
+switch:
+  - platform: output
+    output: output_ph_down
+    id: ph_down
+    name: "pH Down"
+    on_turn_on:
+      - then:
+        - logger.log:
+            level: INFO
+            tag: "dosing"
+            format: "Dosing %d ml of 'pH Down' (%.1f seconds @ %.2f ml/s)"
+            args: [ 'id(pump1_dose_ml)', 'id(pump1_dose_ml) / id(pump1_ml_sec)', 'id(pump1_ml_sec)' ]
+        - delay: !lambda return id(pump1_dose_ml) / id(pump1_ml_sec) * 1000;
+        - switch.turn_off: ph_down
+    on_turn_off:
+      - then:
+        - logger.log: "Stopped dosing 'pH Down'"
+  - platform: output
+    output: output_ph_up
+    id: ph_up
+    name: "pH Up"
+    on_turn_on:
+      - then:
+        - logger.log:
+            level: INFO
+            tag: "dosing"
+            format: "Dosing %d ml of 'pH Up' (%.1f seconds @ %.2f ml/s)"
+            args: [ 'id(pump2_dose_ml)', 'id(pump2_dose_ml) / id(pump2_ml_sec)', 'id(pump2_ml_sec)' ]
+        - delay: !lambda return id(pump2_dose_ml) / id(pump2_ml_sec) * 1000;
+        - switch.turn_off: ph_up
+    on_turn_off:
+      - then:
+        - logger.log: "Stopped dosing 'pH Up'"
diff --git a/packages/secrets.yaml.dist b/packages/secrets.yaml.dist
index 458c71ccc36d298fa167bbf386d51db867e32cf1..a537f167c4ae2950a117b56749157ef164873b68 100644
--- a/packages/secrets.yaml.dist
+++ b/packages/secrets.yaml.dist
@@ -4,3 +4,7 @@ ota_password: ""
 api_password: ""
 domain: "local"
 timezone: "Europe/Berlin"
+pump1_ml_sec: "2.15"    # millilitre per second
+pump1_dose_ml: "10"     # millilitre per dose
+pump2_ml_sec: "2.15"    # millilitre per second
+pump2_dose_ml: "10"     # millilitre per dose