πΎ Archived View for m0yng.uk βΊ Chickens captured on 2022-07-16 at 13:28:17. Gemini links have been rewritten to link to archived content
β¬ οΈ Previous capture (2022-06-03)
β‘οΈ Next capture (2023-01-29)
-=-=-=-=-=-=-
Modified 2022-02-18
Sadly we no longer have chickens.
In 2021 Kitty hatched three chicks, but only one was female and kept, we called her "Alison". Sadly Alison died in January 2022 of unknown reasons, followed a few weeks later by Fanny for possible neurological reasons. We felt it was unfair to keep just one chicken as they are social animals, and have sent Kitty to live with a friend who has other chickens. I'll soon be dismantling this chicken spy system and installing it for that friend to spy on their chickens!
We have three chickens, all are bantams (aka small). We got them in early October 2020 when they were around 10 weeks old and still fairly small (all under 500g) they have all since grown quite a lot (Fanny weighs nearly a kilo!)
We keep them as pets, they are great to stroke and watch as they forage around the garden. Many people ask about eggs, and yes they will lay eggs, but that's not why we have them. As they are bantams, the eggs are much smaller than those you get in the shops.
It's hard to get a good photo of chickens, they move a lot! Here they are still fairly young,
Fanny is a Partridge (medium brown speckled), and the biggest. She started laying in February 2021, her egg shells are light cream.
Kitty is a Speckled (black with white, the black feathers are actually iridescent and often look green or blue in certain lights). She started laying in early March 2021, her egg shells are slightly darker than Fanny's.
Mary is a Buff (golden brown).
Sadly she became very ill and we weren't able to help her get better despite help from the vet. We let her go in early May 2021.
The chickens are all named after ghosts in the TV Show Ghosts
Because I am clearly a nerd, I've been semi-irregularly weighing the chickens. So please enjoy this table of how fat they are (all weights in grams.)
ββββββββββββββ¬ββββββββ¬βββββββ¬ββββββββ β Date β Fanny β Mary β Kitty β ββββββββββββββͺββββββββͺβββββββͺββββββββ‘ β 2021-04-24 β 1040 β 530 β 835 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2021-04-13 β 965 β 560 β 780 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2021-04-02 β 980 β 615 β 755 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2021-03-06 β 940 β 680 β 790 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2021-02-17 β 915 β 650 β 650 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2021-01-18 β 850 β 640 β 600 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-12-24 β 690 β 595 β 565 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-12-14 β 685 β 550 β 570 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-12-08 β 670 β 535 β 550 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-12-01 β 665 β 510 β 550 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-10-23 β 605 β 490 β 515 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-10-18 β 610 β 485 β 505 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-10-11 β 565 β 445 β 470 β ββββββββββββββΌββββββββΌβββββββΌββββββββ€ β 2020-10-02 β 545 β 400 β 455 β ββββββββββββββ΄ββββββββ΄βββββββ΄ββββββββ
Obviously you need to be able to spy on your chickens, and we have a complex solution involving a lunch box and cable ties.
The core of the spying is a Raspberry Pi 3b, which has an Infra-Red fish eye camera attached, a usb webcam, and a DHT22 temperature/humidity sensor. This allows me to connect to an HTTP server and see a live feed from inside the coop (even at night) and into the run, as well as the temperature and humidity inside the coop.
I found the Pi couldn't reliably power itself, a IR camera, and a usb camera without help. To help I added a powered USB hub which provides power to the usb camera and things seem stable this way.
I've drilled two holes into a plastic lunch box, which allow me to pass power into the box (although I did remove the plug and just passed the cable through) and the temperature sensor out again. On the front I found I needed to drill a hole for the camera lens so it has a clear view and I happened to have a drill bit that matched the size of the lens and it wedges in nicely.
With everything shoved into the box, it is secured into the coop using a fat cable tie.
Face on view of the camera in a plastic box [IMG]
Top view of the camera and raspberry pi in a plastic box [IMG]
I want to view the camera "live" and there are a few options for this. I found that motion was ok, but not amazing. I found that m-jpeg streamer[1] works well. I run the command twice to get both the coop camera and the run camera, and I have a script that runs at startup and looks like this:
1: https://github.com/jacksonliam/mjpg-streamer
#!/bin/bash /usr/local/bin/mjpg_streamer -i "/usr/local/lib/mjpg-streamer/input_raspicam.so -rot 180 -x 1000 -y 1000" -o "/usr/local/lib/mjpg-streamer/output_http.so -w /usr/local/share/mjpg-streamer/www/" & sleep 30 /usr/local/bin/mjpg_streamer -i "/usr/local/lib/mjpg-streamer/input_uvc.so -r HD -d /dev/v4l/by-id/usb-GENERAL_GENERAL_WEBCAM-video-index0" -o "/usr/local/lib/mjpg-streamer/output_http.so -w /usr/local/share/mjpg-streamer/www/ -p 8081" &
Interestingly the camera lets me pick seemingly arbitrary resolutions, which means I can pick a 1000x1000 resolution which works really well to show where the chickens are rather than walls and floor π
I also have some python that reads the temperature data
import json import datetime import Adafruit_DHT DHT_SENSOR = Adafruit_DHT.DHT22 DHT_PIN = 4 data = {"temp":"unknown", "humidity":"unknown", "timestamp": datetime.datetime.now().isoformat()} humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN) if humidity is not None and temperature is not None: data = {"temp":temperature, "humidity":humidity, "timestamp": datetime.datetime.now().isoformat()} print(json.dumps(data))
This is run every minute using cron
* * * * * python3 /home/pi/coop/temp.py > /var/www/html/environment.json
THEN I use lighttpd to wrap all this together into a nice little webpage! I won't copy all the code here, but it's basically two image tags and some javascript which pulls the json data and updates it using setInterval.
<img src="http://pidgey:8080/?action=stream" alt="Coop Camera" /> <img src="http://pidgey:8081/?action=stream" alt="Run Camera" />
My naming scheme on the home network is PokΓ©mon and I set the IP address to match the ID in the PokΓ©Dex, which gives me around 255 possible names to pick from. There is no chicken PokΓ©mon in the first 255 so I've named this Pi **Pidgey**.
The CDS (Chicken Detection System) is intended to detect the presence of chickens on their roost, so we can make decisions like "is it time to lock them up for the night".
It uses two 5kg load cells to detect weight on the roost bar, which has the added advantage of allowing easy monitoring of their weight over time.
I found this Raspberry Pi Weight Sensor Software[2] guide very helpful.
2: https://tutorials-raspberrypi.com/digital-raspberry-pi-scale-weight-sensor-hx711/
Both load cells have their four wires extended via external grade CAT5 cable (because I had a reel of it spare), the "excitation" wires are combined and shared across both load cells, and all the wires are connected thus:
Each cell was then "calibrated" by weighing a known mass and doing some maths, e.g. 135g mass gave a reading around 14200. Reading divided by mass gives the "reference value" of around 105.
Each cell has a different reference value which is set as such:
hx.set_reference_unit_A(405) hx.set_reference_unit_B(103)
A key thing is to TARE or not to TARE. The example code I started with would TARE every time it is run, which is great for testing and examples. However it does mean it would only ever return a zero weight, as the chickens are unlikely to have moved on or off between the TARE and the reading. For now, I've done it once when I knew the roost was empty and my code does not run the TARE function. It doesn't seem to remember this "zero value" between runs but that's probably ok, I may add some routine TARE runs when the roost is highly likely to be empty, e.g. in the middle of the day which get an average empty measurement and add (well, ok, subtract) that as a correction to the normal readings.
The readings don't seem very stable, each reading is different and vary by hundreds of grams (e.g. 3.22kg and a few minutes later, 3.44kg). Then again, the chickens do move around, but it shouldn't change the measurements that much. However, if we go back the initial requirement of "detect chickens" it does fulfil the requirement because the readings with chickens are significantly higher than without, so it can absolutely "detect chickens".
Now I need to work out what to do with this detection.
For now I've also installed influxDB and Graphana so I have somewhere to shove the data and view it easily, this is almost certainly overkill but it was a quick way to do something with the data. For now I have a graph that shows the mass measured by each load cell and a combined value, all are on a moving average of 15 minutes which helps give a slightly smoother view of the data. The night average is around 3.2kg, and daytime around 430g. Which if we "manually TARE" gives around 2770g, if we then subtract the "known chicken weights" we are around 360g high. Which is fairly high, if you consider they each chicken weighed around that when we first got them! However, it does still work to detect the presence of chickens.
I've used graphana's alerting capability to tie this all together (with some python obviously!) If the total weight on the roost exceeds 2kg for over 15 minutes (to avoid false alerts if a bird just hops on for a moment) it will ping a webhook (specifically it will POST some JSON to a specified URI with details of the alert) which is a simple bit of python listening for that alert and preset to send an XMPP message to me. And it works!
The python code is embarrassingly simple IMO, but that's the power of open source letting me leverage existing code.
#!/usr/bin/env python3 from flask import Flask, request, Response import configparser import xmpp config = configparser.ConfigParser() config.read('config.ini') jid = xmpp.protocol.JID(config['xmpp']['username']) connection = xmpp.Client(server=jid.getDomain()) connection.connect() connection.auth(user=jid.getNode(), password=config['xmpp']['password'], resource=jid.getResource()) app = Flask(__name__) @app.route('/webhook', methods=['POST']) def respond(): print(request.json) message = request.json['message'] + ' in state ' + request.json['state'] connection.send(xmpp.protocol.Message(to=config['xmpp']['receiver'], body=message)) return Response(status=200)
The python that runs every minute was extended, and now looks like this mess:
import json import datetime import Adafruit_DHT import RPi.GPIO as GPIO from hx711 import HX711 from influxdb import InfluxDBClient DHT_SENSOR = Adafruit_DHT.DHT22 DHT_PIN = 4 data = {"temp": "unknown", "humidity": "unknown", "timestamp": datetime.datetime.now().isoformat()} now = datetime.datetime.now().isoformat() humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN) hx = HX711(5, 6) hx.set_reading_format("MSB", "MSB") hx.set_reference_unit_A(405) hx.set_reference_unit_B(103) hx.reset() val_A = hx.get_weight_A(5) val_B = hx.get_weight_B(5) val_C = val_A + val_B hx.power_down() GPIO.cleanup() if humidity is not None and temperature is not None: data = {"temp": temperature, "humidity": humidity, "detector": round(val_C), "timestamp": now} json_body = [ { "measurement": "temperature", "tags": { "location": "coop" }, "time": now, "fields": { "value": data["temp"] } }, { "measurement": "humidity", "tags": { "location": "coop" }, "time": now, "fields": { "value": data["humidity"] } }, { "measurement": "detector", "tags": { "location": "coop" }, "time": now, "fields": { "value": val_C } }, { "measurement": "detector", "tags": { "location": "A" }, "time": now, "fields": { "value": val_A } }, { "measurement": "detector", "tags": { "location": "B" }, "time": now, "fields": { "value": val_B } } ] print(json.dumps(data)) client = InfluxDBClient('localhost', 8086, None, None, 'chickens') client.write_points(json_body)
-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+-
π€ Black Lives Matter
ππ€π Trans Rights are Human Rights
β€οΈπ§‘ππππ Love is Love
Copyright Β© 2022 Christopher M0YNG
Code snippets are licenced under the Hippocratic License 3.0 (or later.)
Page generated 2022-07-11 by Complex 19