├── .gitignore ├── README.md ├── examples ├── home-assistant │ ├── README.md │ └── temperature.py ├── openhab2 │ ├── README.md │ └── temperature.py ├── telegram-bot │ └── indoor.py └── webthings │ ├── README.md │ ├── ds18b20-sensor.py │ ├── multi-sensor.py │ └── sht20-sensor.py ├── sensor ├── BMP180.py ├── DS18B20.py ├── HTU21D.py ├── MCP3004.py ├── SHT20.py ├── __init__.py └── util.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | sensor.egg-info/ 3 | dist/ 4 | todo.txt 5 | .remote-sync.json 6 | configuration.yaml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Sensors 2 | 3 | This is a **Python 3** package that enables **Raspberry Pi** to read various 4 | sensors. 5 | 6 | Supported devices include: 7 | - **DS18B20** temperature sensor 8 | - **BMP180** pressure and temperature sensor 9 | - **HTU21D** humidity and temperature sensor 10 | - **SHT20** humidity and temperature sensor 11 | - **MCP3004** A/D Converter (**MCP3008** also compatible) 12 | 13 | The chief motivation for this package is educational. I am teaching a Raspberry 14 | Pi course, and find it very troublesome for students having to download a 15 | separate library every time they use another sensor. With this package, download 16 | once and they are set (for my course, anyway). I hope you find it useful, too. 17 | 18 | ## Installation 19 | 20 | It is best to update Linux first. 21 | 22 | `sudo apt-get update` 23 | `sudo apt-get dist-upgrade` 24 | 25 | Install this package: 26 | 27 | `sudo pip3 install sensor` 28 | 29 | But the `sensor` package would not work by itself. Communicating with sensors 30 | often requires some sort of serial protocol, such as **1-wire**, **I2C**, or 31 | **SPI**. You have to know which sensor speaks which, and set up Raspberry Pi to 32 | do so. 33 | 34 | ## Enable 1-Wire, I2C, or SPI 35 | 36 | `sudo raspi-config`, enter **Interfacing Options**, enable the protocols you 37 | need. 38 | 39 | ## Know your sensor's address 40 | 41 | Unlike many libraries out there, this library knows **no default bus number** 42 | and **no default device address**. I want learners to be explicitly aware of 43 | those numbers, even if they are fixed. 44 | 45 | For example: 46 | - **I2C** bus is numbered **1** 47 | - **SPI** bus is numbered **0** 48 | 49 | To find out individual sensor's address: 50 | - For 1-wire sensors, go to `/sys/bus/w1/devices/` 51 | - For I2C sensors, use `i2cdetect -y 1` 52 | - For SPI sensors, you should know which CS pin you use 53 | 54 | ## My sensors don't give simple numbers 55 | 56 | Unlike many libraries out there, this library does not return a simple Celcius 57 | degree when reading temperatures, does not return a simple hPa value when 58 | reading pressure, does not return a simple RH% when reading humidity, etc. 59 | Instead, I return a **namedtuple** representing the quantity, which offers two 60 | benefits: 61 | 62 | - No more conversion needed. Suppose you get a *Temperature* called `t`, you may 63 | access the Celcius degree by `t.C` as easily as you do Fahrenheit by `t.F`. 64 | - Namedtuples may have methods. For example, a *Pressure* has a method called 65 | `altitude()`, which tells you how high you are above mean sea level. 66 | 67 | ## DS18B20 68 | 69 | - Temperature, 1-wire 70 | - To find out the sensor's address: 71 | 72 | ``` 73 | $ cd /sys/bus/w1/devices/ 74 | $ ls 75 | 28-XXXXXXXXXXXX w1_bus_master1 76 | ``` 77 | 78 | Read the sensor as follows: 79 | 80 | ```python 81 | from sensor import DS18B20 82 | 83 | ds = DS18B20('28-XXXXXXXXXXXX') 84 | t = ds.temperature() # read temperature 85 | 86 | print(t) # this is a namedtuple 87 | print(t.C) # Celcius 88 | print(t.F) # Fahrenheit 89 | print(t.K) # Kelvin 90 | ``` 91 | 92 | ## BMP180 93 | 94 | - Pressure + Temperature, I2C 95 | - Use `i2cdetect -y 1` to check address. It is probably `0x77`. 96 | 97 | ```python 98 | from sensor import BMP180 99 | 100 | # I2C bus=1, Address=0x77 101 | bmp = BMP180(1, 0x77) 102 | 103 | p = bmp.pressure() # read pressure 104 | print(p) # namedtuple 105 | print(p.hPa) # hPa value 106 | 107 | t = bmp.temperature() # read temperature 108 | print(t) # namedtuple 109 | print(t.C) # Celcius degree 110 | 111 | p, t = bmp.all() # read both at once 112 | print(p) # Pressure namedtuple 113 | print(t) # Temperature namedtuple 114 | 115 | # Look up mean sea level pressure from local observatory. 116 | # 1009.1 hPa is only for example. 117 | a = p.altitude(msl=1009.1) 118 | 119 | print(a) # Altitude 120 | print(a.m) # in metre 121 | print(a.ft) # in feet 122 | ``` 123 | 124 | ## HTU21D 125 | 126 | - Humidity + Temperature, I2C 127 | - Use `i2cdetect -y 1` to check address. It is probably `0x40`. 128 | 129 | ```python 130 | from sensor import HTU21D 131 | 132 | # I2C bus=1, Address=0x40 133 | htu = HTU21D(1, 0x40) 134 | 135 | h = htu.humidity() # read humidity 136 | print(h) # namedtuple 137 | print(h.RH) # relative humidity 138 | 139 | t = htu.temperature() # read temperature 140 | print(t) # namedtuple 141 | print(t.F) # Fahrenheit 142 | 143 | h, t = htu.all() # read both at once 144 | ``` 145 | 146 | ## SHT20 147 | 148 | - Humidity + Temperature, I2C 149 | - Use `i2cdetect -y 1` to check address. It is probably `0x40`. 150 | 151 | ```python 152 | from sensor import SHT20 153 | 154 | # I2C bus=1, Address=0x40 155 | sht = SHT20(1, 0x40) 156 | 157 | h = sht.humidity() # read humidity 158 | print(h) # namedtuple 159 | print(h.RH) # relative humidity 160 | 161 | t = sht.temperature() # read temperature 162 | print(t) # namedtuple 163 | print(t.C) # Celsius 164 | 165 | h, t = sht.all() # read both at once 166 | ``` 167 | 168 | ## MCP3004 169 | 170 | - Analog sensors (e.g. photoresistor) cannot interface with Raspberry Pi 171 | directly. They have to go through an A/D converter. 172 | 173 | ```python 174 | from sensor import MCP3004 175 | 176 | # SPI bus=0, CS=0, V_ref=3.3V 177 | mcp = MCP3004(bus=0, addr=0, vref=3.3) 178 | 179 | mcp.voltage(0) # read voltage on channel 0 180 | ``` 181 | -------------------------------------------------------------------------------- /examples/home-assistant/README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Integration 2 | 3 | [Home Assistant](https://home-assistant.io) is an open-source home automation 4 | platform. This page describes how I integrate various components into it. Home 5 | Assistant version is **0.95.4**, released on June 29, 2019. 6 | 7 | ## Installation 8 | 9 | ``` 10 | sudo apt-get update 11 | sudo apt-get dist-upgrade 12 | 13 | ### Home Assistant needs Python 3.6 or later ### 14 | ### No need for Buster, because Buster has Python 3.7 as default ### 15 | 16 | sudo apt-get install libssl-dev openssl libreadline-dev libffi-dev libsqlite3-dev 17 | wget https://www.python.org/ftp/python/3.7.4/Python-3.7.4.tgz 18 | tar zxf Python-3.7.4.tgz 19 | cd Python-3.7.4 20 | ./configure --enable-loadable-sqlite-extensions 21 | make -j4 22 | sudo make install 23 | 24 | ### Install Home Assistant ### 25 | 26 | sudo pip3 install pip wheel --upgrade 27 | sudo pip3 install homeassistant 28 | 29 | hass # start Home Assistant webserver 30 | ``` 31 | 32 | The first time running `hass` takes a while because it has to install a few more 33 | Python packages. After a while, you should be able to access it by pointing your 34 | browser to `http://:8123` 35 | 36 | If anything fails to download, try to `wget` it manually, then install it 37 | locally: 38 | 39 | ``` 40 | wget zzzzzz.whl 41 | sudo pip3 install zzzzzz.whl 42 | ``` 43 | 44 | If you see this error: 45 | 46 | ``` 47 | ImportError: /usr/lib/arm-linux-gnueabihf/libssl.so.1.1: version `OPENSSL_1_1_1' not found 48 | ``` 49 | 50 | Try the following: 51 | 52 | ``` 53 | sudo pip3 uninstall cryptography 54 | 55 | ### Edit file /etc/pip.conf to not use piwheels. Comment out all lines. 56 | 57 | sudo pip3 install cryptography 58 | 59 | ### Edit file /etc/pip.conf back to using piwheels. 60 | ``` 61 | 62 | Sensor integration is done by modifying the file `/home/pi/.homeassistant/configuration.yaml` 63 | 64 | At any time, you may check the validity of the config file with: 65 | 66 | ``` 67 | hass --script check_config --info all 68 | ``` 69 | 70 | ## Remove unwanted icons 71 | 72 | ``` 73 | homeassistant: 74 | customize: 75 | person.pi: 76 | hidden: true 77 | sun.sun: 78 | hidden: true 79 | weather.home: 80 | hidden: true 81 | ``` 82 | 83 | ## DS18B20 as [Command Line Sensor](https://home-assistant.io/components/sensor.command_line/) 84 | 85 | Home Assistant has this concept of a Command Line Sensor. It allows you to 86 | integrate any type of sensor as long as the sensor's data can be read from the 87 | command line. 88 | 89 | The script [temperature.py](./temperature.py) displays the reading of DS18B20. 90 | It can serve as a Command Line Sensor. Add these lines to `configuration.yaml`: 91 | 92 | ``` 93 | sensor: 94 | - platform: command_line 95 | name: DS18B20 Sensor 96 | command: python3 /path/to/temperature.py 97 | value_template: '{{ value | round(1) }}' 98 | unit_of_measurement: "°C" 99 | scan_interval: 3 100 | ``` 101 | 102 | ## [TP-Link Smart Home Devices](https://home-assistant.io/components/tplink/) 103 | 104 | I have tried **LB100** Smart LED Bulb and **HS100** Smart Plug. 105 | 106 | ``` 107 | tplink: 108 | discovery: false 109 | switch: 110 | - host: 192.168.0.103 111 | light: 112 | - host: 192.168.0.104 113 | ``` 114 | 115 | ## Automation: Turn ON/OFF Smart Plug depending on Temperature 116 | 117 | First, comment out the default include. We are going to put the automation 118 | section in the same file: 119 | 120 | ``` 121 | # automation: !include automations.yaml 122 | ``` 123 | 124 | **For everything below, adjust `entity_id` accordingly.** 125 | 126 | ``` 127 | automation: 128 | - alias: Turn ON fan if too hot 129 | trigger: 130 | platform: numeric_state 131 | entity_id: sensor.ds18b20_sensor 132 | above: 31.5 133 | action: 134 | service: switch.turn_on 135 | entity_id: switch.fan_plug 136 | 137 | - alias: Turn OFF fan if too cool 138 | trigger: 139 | platform: numeric_state 140 | entity_id: sensor.ds18b20_sensor 141 | below: 31 142 | action: 143 | service: switch.turn_off 144 | entity_id: switch.fan_plug 145 | ``` 146 | 147 | ## Automation: Send a [Telegram](https://home-assistant.io/components/notify.telegram/) message when Temperature gets too hot 148 | 149 | ``` 150 | telegram_bot: 151 | - platform: polling 152 | api_key: ..........TOKEN.......... 153 | allowed_chat_ids: 154 | - 999999999 155 | 156 | notify: 157 | - name: telegram 158 | platform: telegram 159 | chat_id: 999999999 160 | 161 | automation: 162 | - alias: Notify me if too hot 163 | trigger: 164 | platform: numeric_state 165 | entity_id: sensor.ds18b20_sensor 166 | above: 32 167 | action: 168 | service: notify.telegram 169 | data: 170 | message: "\U0001f525" # Fire emoji 171 | ``` 172 | 173 | ## [Sun Trigger](https://home-assistant.io/docs/automation/trigger/#sun-trigger) and [Time Trigger](https://home-assistant.io/docs/automation/trigger/#time-trigger) 174 | 175 | ## Run on Startup 176 | 177 | To run programs on startup, we create systemd services. 178 | 179 | In directory `/lib/systemd/system`, create a file `homeassistant.service` and 180 | insert the following contents: 181 | 182 | ``` 183 | [Unit] 184 | Description=Home Assistant 185 | After=network.target 186 | 187 | [Service] 188 | ExecStart=/usr/local/bin/hass 189 | User=pi 190 | 191 | [Install] 192 | WantedBy=multi-user.target 193 | ``` 194 | 195 | Then, you can control the service using `systemctl`: 196 | 197 | To run it manually: `sudo systemctl start homeassistant.service` 198 | To stop it manually: `sudo systemctl stop homeassistant.service` 199 | To check its status: `sudo systemctl status homeassistant.service` 200 | To make it run on startup: `sudo systemctl enable homeassistant.service` 201 | To stop it from running on startup: `sudo systemctl disable homeassistant.service` 202 | 203 | ## Dynamic DNS and SSL Certificate 204 | 205 | This [blog post](https://home-assistant.io/blog/2015/12/13/setup-encryption-using-lets-encrypt/) 206 | tells you how to set up Dynamic DNS and SSL certificate. Its instructions are not 207 | tailored to Raspberry Pi and seem a little out-of-date. I summarize my 208 | experiences below: 209 | 210 | 1. Obtain a domain on **[Duck DNS](https://www.duckdns.org)** 211 | 212 | 2. Follow the **[installation instructions](https://www.duckdns.org/install.jsp)**. 213 | Choose **pi** for Raspberry Pi-specific instructions. Basically, this is what 214 | you do: 215 | - Create a shell script which updates the IP address of your Duck DNS domain 216 | - Set up a cron job to run the shell script every 5 minutes 217 | 218 | After this, Dynamic DNS is completely set up. You have your own domain, and 219 | you can use that domain to reach your home's router. Next, we set up an SSL 220 | certificate to encrypt all HTTP communications. 221 | 222 | 3. We use a software utility called **[Certbot](https://certbot.eff.org)** to 223 | obtain a 90-day SSL certificate from **[Let's Encrypt](https://letsencrypt.org/how-it-works/)**. 224 | 225 | During the process, Certbot will spin up a temporary webserver on Raspberry 226 | Pi's port 80 for Let's Encrypt to verify the control of domain. Normally, 227 | incoming traffic cannot get through the router, unless a port-forward exists. 228 | 229 | So, set up a **port-forward `Router port 80` → `Pi Port 80`**. 230 | 231 | Install certbot and use it to obtain a certificate: 232 | 233 | ``` 234 | sudo apt-get install certbot 235 | sudo certbot certonly --standalone -d yourdomain.duckdns.org 236 | ``` 237 | 238 | Resulting contents are put in the directory `/etc/letsencrypt`. 239 | Certificate-related files are in `/etc/letsencrypt/live`. Most stuff there 240 | are accessible by root only. To be used by Home Assistant, permission has to 241 | be loosen: 242 | 243 | ``` 244 | cd /etc/letsencrypt 245 | sudo chmod +rx live archive 246 | ``` 247 | 248 | The SSL certificate is now ready. 249 | 250 | 4. Tell Home Assistant about where the certificate-related files are. Insert 251 | the following lines into `configuration.yaml`, which include a password to 252 | protect the website: 253 | 254 | ``` 255 | http: 256 | ssl_certificate: /etc/letsencrypt/live/yourdomain.duckdns.org/fullchain.pem 257 | ssl_key: /etc/letsencrypt/live/yourdomain.duckdns.org/privkey.pem 258 | base_url: yourdomain.duckdns.org 259 | ``` 260 | 261 | Finally, we are ready to access Home Assistant on **HTTPS**. HTTPS runs on 262 | port 443, while Home Assistant starts on port 8123. So, modify the router's 263 | **port-forward `Router port 443` → `Pi Port 8123`**. 264 | 265 | On the browser, remember to explicitly **force the communication protocol by 266 | typing `https://` before the domain**. Otherwise, the browser may default to 267 | contacting port 80 (HTTP), which should be blocked by the router. 268 | -------------------------------------------------------------------------------- /examples/home-assistant/temperature.py: -------------------------------------------------------------------------------- 1 | from sensor import DS18B20 2 | 3 | ds = DS18B20('28-00000736781c') 4 | 5 | print(ds.temperature().C) 6 | -------------------------------------------------------------------------------- /examples/openhab2/README.md: -------------------------------------------------------------------------------- 1 | # openHAB Integration 2 | 3 | Platform: Raspbian Stretch 4 | 5 | ## Install latest Java 8 version 6 | 7 | OpenHAB requires Java 8 and recommends **at least revision "101"**. Check your 8 | version: 9 | 10 | ``` 11 | $ java -version 12 | ``` 13 | 14 | Raspbian Stretch likely comes with a lower revision than "101". We need to 15 | install a newer JDK. Instructions below are taken from 16 | [ribasco](https://gist.github.com/ribasco/fff7d30b31807eb02b32bcf35164f11f). 17 | 18 | 1. Create a file `key.txt` and insert the following lines of text: 19 | 20 | ``` 21 | -----BEGIN PGP PUBLIC KEY BLOCK----- 22 | Version: SKS 1.1.5 23 | Comment: Hostname: keyserver.ubuntu.com 24 | 25 | mI0ES9/P3AEEAPbI+9BwCbJucuC78iUeOPKl/HjAXGV49FGat0PcwfDd69MVp6zUtIMbLgkU 26 | OxIlhiEkDmlYkwWVS8qy276hNg9YKZP37ut5+GPObuS6ZWLpwwNus5PhLvqeGawVJ/obu7d7 27 | gM8mBWTgvk0ErnZDaqaU2OZtHataxbdeW8qH/9FJABEBAAG0DUxhdW5jaHBhZCBWTEOImwQQ 28 | AQIABgUCVsN4HQAKCRAEC6TrO3+B2tJkA/jM3b7OysTwptY7P75sOnIu+nXLPlzvja7qH7Wn 29 | A23itdSker6JmyJrlQeQZu7b9x2nFeskNYlnhCp9mUGu/kbAKOx246pBtlaipkZdGmL4qXBi 30 | +bi6+5Rw2AGgKndhXdEjMxx6aDPq3dftFXS68HyBM3HFSJlf7SmMeJCkhNRwiLYEEwECACAF 31 | Akvfz9wCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRDCUYJI7qFIhucGBADQnY4V1xKT 32 | 1Gz+3ERly+nBb61BSqRx6KUgvTSEPasSVZVCtjY5MwghYU8T0h1PCx2qSir4nt3vpZL1luW2 33 | xTdyLkFCrbbIAZEHtmjXRgQu3VUcSkgHMdn46j/7N9qtZUcXQ0TOsZUJRANY/eHsBvUg1cBm 34 | 3RnCeN4C8QZrir1CeA== 35 | =CziK 36 | -----END PGP PUBLIC KEY BLOCK----- 37 | ``` 38 | 39 | 2. Add the key: 40 | 41 | ``` 42 | $ sudo apt-key add key.txt 43 | ``` 44 | 45 | 3. Add the repository. Create a file `webupd8team-java.list` in the directory 46 | `/etc/apt/sources.list.d/`, and insert the following lines: 47 | 48 | ``` 49 | deb http://ppa.launchpad.net/webupd8team/java/ubuntu xenial main 50 | deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu xenial main 51 | ``` 52 | 53 | 4. Tell `apt-get` about the new repository and install: 54 | 55 | ``` 56 | $ sudo apt-get update 57 | $ sudo apt-get install oracle-java8-installer 58 | ``` 59 | 60 | 5. Verify: 61 | 62 | ``` 63 | $ java -version 64 | ``` 65 | 66 | ## Install openHAB 2 67 | 68 | Steps are similar to above. Instructions are summarized from [openHAB 69 | installation guide](https://www.openhab.org/docs/installation/linux.html). 70 | 71 | 1. Add the key: 72 | 73 | ``` 74 | $ wget -qO - 'https://bintray.com/user/downloadSubjectPublicKey?username=openhab' | sudo apt-key add - 75 | $ sudo apt-get install apt-transport-https 76 | ``` 77 | 78 | 2. Add the repository: 79 | 80 | ``` 81 | $ echo 'deb https://dl.bintray.com/openhab/apt-repo2 stable main' | sudo tee /etc/apt/sources.list.d/openhab2.list 82 | ``` 83 | 84 | 3. Install: 85 | 86 | ``` 87 | $ sudo apt-get update 88 | $ sudo apt-get install openhab2 89 | ``` 90 | 91 | 4. Start openHAB and install the Standard setup: 92 | 93 | ``` 94 | $ sudo systemctl start openhab2.service 95 | ``` 96 | 97 | The first start may take a while. After a while, you should be able to access it 98 | at: `http://:8080` 99 | 100 | **Choose the Standard setup**. 101 | 102 | ### How to monitor? 103 | 104 | Logs are stored in the directory `/var/log/openhab2/`. You can follow openHAB's 105 | activities by monitoring these files: 106 | 107 | ``` 108 | $ tail -f /var/log/openhab2/openhab.log 109 | $ tail -f /var/log/openhab2/events.log 110 | ``` 111 | 112 | ## Configuration workflow 113 | 114 | Integrating a sensor or appliance to openHAB follows a common workflow. 115 | 116 | 1. Use **Paper UI** to add the appropriate *binding* 117 | 118 | 2. Define a *thing* in a `*.things` file in `/etc/openhab2/things/`. A thing 119 | represents a physical entity, e.g. a temperature sensor, a light bulb. 120 | 121 | 3. Define an *item* in a `*.items` file in `/etc/openhab2/items/`. An item 122 | represents an element on the user interface, and is usually linked to a thing's 123 | *channel*. 124 | 125 | 4. Optionally define some *rules* in a `*.rules` file in `/etc/openhab2/rules/`. 126 | Rules govern how the system reacts to changes. They are the "smart" of the 127 | system. 128 | 129 | 5. Put items in a `*.sitemap` file in `/etc/openhab2/sitemaps/`. A *sitemap* 130 | defines how items are laid out on the user interface. 131 | 132 | ## DS18B20 on [Exec Binding](https://www.openhab.org/addons/bindings/exec/) 133 | 134 | Paper UI → Add-ons → Install **Exec Binding** and **RegEx Transformation** 135 | 136 | The Exec binding extracts information by executing a command. So, we make a 137 | [Python script](./temperature.py) that prints out the temperature. Make sure the 138 | script can be run by user `openhab`: 139 | 140 | ``` 141 | $ sudo -u openhab python3 temperature.py 142 | ``` 143 | 144 | #### things 145 | ``` 146 | Thing exec:command:ds18 [ command="/usr/bin/python3 /home/pi/smarthome/temperature.py", interval=5, timeout=3 ] 147 | ``` 148 | 149 | #### items 150 | ``` 151 | String TemperatureStr { channel="exec:command:ds18:output" } 152 | Number Temperature "Temperature [%.1f °C]" 153 | ``` 154 | 155 | #### rules 156 | ``` 157 | rule "Convert temperature string to number" 158 | when 159 | Item TemperatureStr changed 160 | then 161 | val newValue = transform("REGEX", "(\\d*.\\d*).*", TemperatureStr.state.toString) 162 | Temperature.postUpdate(newValue) 163 | end 164 | ``` 165 | 166 | #### home.sitemap 167 | ``` 168 | sitemap home label="Sham Shui Po" { 169 | Frame { 170 | Text item=Temperature 171 | } 172 | } 173 | ``` 174 | 175 | ## SHT20 on [Exec Binding](https://www.openhab.org/addons/bindings/exec/) 176 | 177 | I leave it as an exercise to make a script that prints out humidity. 178 | 179 | If user `openhab` has trouble running the script, it is likely because he is not 180 | included in the appropriate groups. Try: 181 | 182 | ``` 183 | sudo usermod -aG gpio,i2c,spi openhab 184 | ``` 185 | 186 | #### things 187 | ``` 188 | Thing exec:command:sht [ command="/usr/bin/python3 /home/pi/smarthome/humidity.py", interval=5, timeout=3 ] 189 | ``` 190 | 191 | #### items 192 | ``` 193 | String HumidityStr { channel="exec:command:sht:output" } 194 | Number Humidity "Humidity [%.1f %%]" 195 | ``` 196 | 197 | #### rules 198 | ``` 199 | rule "Convert humidity string to number" 200 | when 201 | Item HumidityStr changed 202 | then 203 | val newValue = transform("REGEX", "(\\d*.\\d*).*", HumidityStr.state.toString) 204 | Humidity.postUpdate(newValue) 205 | end 206 | ``` 207 | 208 | #### home.sitemap 209 | ``` 210 | ... { 211 | ... { 212 | Text item=Humidity 213 | } 214 | } 215 | ``` 216 | 217 | ## Use [myopenhab.org](http://www.myopenhab.org/) for remote access 218 | 219 | 1. Paper UI → Add-ons → Misc → Install **openHAB Cloud Connector** 220 | 221 | 2. Paper UI → Configuration → Services → Configure openHAB Cloud 222 | 223 | 3. Register for an account on [myopenhab.org](http://www.myopenhab.org/). You 224 | will be asked for **openHAB UUID** and **openHAB secret**: 225 | 226 | - Find UUID in `/var/lib/openhab2/uuid` 227 | - Find secret in `/var/lib/openhab2/openhabcloud/secret` 228 | 229 | ## LB100 Smart LED Bulb on [TPLinkSmartHome Binding](https://www.openhab.org/addons/bindings/tplinksmarthome/) 230 | 231 | Paper UI → Add-ons → Install **TP-Link Smart Home Binding** 232 | 233 | #### things 234 | ``` 235 | Thing tplinksmarthome:lb100:bookroom_light [ ipAddress="192.168.0.100", refresh=5 ] 236 | ``` 237 | 238 | #### items 239 | ``` 240 | Dimmer BookRoom_Light "Book Room" { channel="tplinksmarthome:lb100:bookroom_light:brightness" } 241 | ``` 242 | 243 | #### home.sitemap 244 | ``` 245 | ... { 246 | ... { 247 | Slider item=BookRoom_Light 248 | } 249 | } 250 | ``` 251 | 252 | ## HS100 Smart Plug on [TPLinkSmartHome Binding](https://www.openhab.org/addons/bindings/tplinksmarthome/) 253 | 254 | #### things 255 | ``` 256 | Thing tplinksmarthome:hs100:fan_plug [ ipAddress="192.168.0.101", refresh=5 ] 257 | ``` 258 | 259 | #### items 260 | ``` 261 | Switch Fan_Plug "Fan" { channel="tplinksmarthome:hs100:fan_plug:switch" } 262 | ``` 263 | 264 | #### home.sitemap 265 | ``` 266 | ... { 267 | ... { 268 | Switch item=Fan_Plug 269 | } 270 | } 271 | ``` 272 | 273 | ## Automation 274 | 275 | #### rules 276 | ``` 277 | rule "Turn on/off fan, adjust light" 278 | when 279 | Item Temperature changed 280 | then 281 | if (Temperature.state > 29) { 282 | Fan_Plug.sendCommand(ON) 283 | BookRoom_Light.sendCommand(10) 284 | } 285 | else if (Temperature.state < 28.5) { 286 | Fan_Plug.sendCommand(OFF) 287 | BookRoom_Light.sendCommand(90) 288 | } 289 | end 290 | ``` 291 | -------------------------------------------------------------------------------- /examples/openhab2/temperature.py: -------------------------------------------------------------------------------- 1 | from sensor import DS18B20 2 | 3 | ds = DS18B20('28-00000736781c') 4 | 5 | print(ds.temperature().C) 6 | -------------------------------------------------------------------------------- /examples/telegram-bot/indoor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import traceback 4 | import telepot 5 | from telepot.loop import MessageLoop 6 | from sensor import DS18B20, SHT20 7 | 8 | """ 9 | $ python3 indoor.py 10 | 11 | An indoor climate monitor with 2 sensors. 12 | 13 | It also comes with a Telegram bot that can report data periodically. 14 | 15 | To know more about Telegram Bot and telepot, go to: 16 | https://github.com/nickoala/telepot 17 | """ 18 | 19 | ds = DS18B20('28-03199779f5a1') 20 | sht = SHT20(1, 0x40) 21 | 22 | def read_all(): 23 | return ds.temperature(), sht.humidity() 24 | 25 | # Read sensors and send to user 26 | def read_send(chat_id): 27 | t, h = read_all() 28 | msg = '{:.1f}°C {:.1f}%'.format(t.C, h.RH) 29 | bot.sendMessage(chat_id, msg) 30 | 31 | def handle(msg): 32 | global last_report, report_interval 33 | 34 | msg_type, chat_type, chat_id = telepot.glance(msg) 35 | 36 | # ignore non-text message 37 | if msg_type != 'text': 38 | print('Non-text message. Ignore.') 39 | return 40 | 41 | # only respond to one user 42 | if chat_id != USER_ID: 43 | print('Message from unknown user. Ignore.') 44 | return 45 | 46 | command = msg['text'].strip().lower() 47 | 48 | if command == '/now': 49 | read_send(chat_id) 50 | elif command == '/1m': 51 | read_send(chat_id) 52 | last_report = time.time() 53 | report_interval = 60 # report every 60 seconds 54 | elif command == '/1h': 55 | read_send(chat_id) 56 | last_report = time.time() 57 | report_interval = 3600 # report every 3600 seconds 58 | elif command == '/cancel': 59 | last_report = None 60 | report_interval = None # clear periodic reporting 61 | bot.sendMessage(chat_id, "OK") 62 | else: 63 | bot.sendMessage(chat_id, "I don't understand") 64 | 65 | 66 | TOKEN = sys.argv[1] 67 | USER_ID = int(sys.argv[2]) 68 | 69 | bot = telepot.Bot(TOKEN) 70 | 71 | MessageLoop(bot, handle).run_as_thread() 72 | print('Listening ...') 73 | 74 | # variables for periodic reporting 75 | last_report = None 76 | report_interval = None 77 | 78 | while 1: 79 | # Is it time to report again? 80 | now = time.time() 81 | if (report_interval is not None 82 | and last_report is not None 83 | and now - last_report >= report_interval): 84 | try: 85 | read_send(USER_ID) 86 | last_report = now 87 | except: 88 | traceback.print_exc() 89 | 90 | time.sleep(1) 91 | -------------------------------------------------------------------------------- /examples/webthings/README.md: -------------------------------------------------------------------------------- 1 | # WebThings Integration 2 | 3 | Background: 4 | 5 | - [WebThings Framework](https://webthings.io/framework/) 6 | 7 | - [Python Library](https://github.com/WebThingsIO/webthing-python) 8 | 9 | 10 | Install: 11 | 12 | ``` 13 | sudo pip3 install webthing 14 | ``` 15 | 16 | [Systemd](https://www.raspberrypi.org/documentation/linux/usage/systemd.md) 17 | service file in `/etc/systemd/system/`: 18 | 19 | ``` 20 | [Unit] 21 | Description=WebThing Server 22 | After=network.target 23 | 24 | [Service] 25 | ExecStart=python3 /PATH/TO/SCRIPT 26 | User=pi 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/webthings/ds18b20-sensor.py: -------------------------------------------------------------------------------- 1 | from webthing import Thing, Property, Value, SingleThing, WebThingServer 2 | import logging 3 | import tornado.ioloop 4 | from sensor import DS18B20 5 | 6 | 7 | def run_server(): 8 | ds18 = DS18B20('28-03199779f5a1') 9 | celsius = Value(ds18.temperature().C) 10 | 11 | thing = Thing( 12 | 'urn:dev:ops:temperature-sensor', 13 | 'DS18B20', 14 | ['TemperatureSensor']) 15 | 16 | thing.add_property( 17 | Property( 18 | thing, 19 | 'celsius', 20 | celsius, 21 | metadata={ 22 | '@type': 'TemperatureProperty', 23 | 'title': 'Celsius', 24 | 'type': 'number', 25 | 'unit': '°C', 26 | 'readOnly': True })) 27 | 28 | server = WebThingServer(SingleThing(thing), port=8888) 29 | 30 | def update(): 31 | t = ds18.temperature() 32 | celsius.notify_of_external_update(t.C) 33 | 34 | timer = tornado.ioloop.PeriodicCallback(update, 3000) 35 | timer.start() 36 | 37 | try: 38 | logging.info('starting the server') 39 | server.start() 40 | except KeyboardInterrupt: 41 | logging.debug('stopping update task') 42 | timer.stop() 43 | logging.info('stopping the server') 44 | server.stop() 45 | logging.info('done') 46 | 47 | 48 | if __name__ == '__main__': 49 | logging.basicConfig( 50 | level=10, 51 | format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s" 52 | ) 53 | run_server() 54 | -------------------------------------------------------------------------------- /examples/webthings/multi-sensor.py: -------------------------------------------------------------------------------- 1 | from webthing import Thing, Property, Value, MultipleThings, WebThingServer 2 | import logging 3 | import tornado.ioloop 4 | from sensor import DS18B20, SHT20 5 | 6 | 7 | def run_server(): 8 | ds18 = DS18B20('28-03199779f5a1') 9 | ds18_celsius = Value(ds18.temperature().C) 10 | 11 | ds18_thing = Thing( 12 | 'urn:dev:ops:temperature-sensor', 13 | 'ds18b20', 14 | ['TemperatureSensor']) 15 | 16 | ds18_thing.add_property( 17 | Property( 18 | ds18_thing, 19 | 'celsius', 20 | ds18_celsius, 21 | metadata={ 22 | '@type': 'TemperatureProperty', 23 | 'title': 'Celsius', 24 | 'type': 'number', 25 | 'unit': '°C', 26 | 'readOnly': True })) 27 | 28 | sht = SHT20(1, 0x40) 29 | h, t = sht.all() 30 | sht_celsius = Value(t.C) 31 | sht_rh = Value(h.RH) 32 | 33 | sht_thing = Thing( 34 | 'urn:dev:ops:humidity-temperature-sensor', 35 | 'sht20', 36 | ['MultiLevelSensor', 'TemperatureSensor']) 37 | 38 | sht_thing.add_property( 39 | Property( 40 | sht_thing, 41 | 'humidity', 42 | sht_rh, 43 | metadata={ 44 | '@type': 'LevelProperty', 45 | 'title': 'Relative humidity', 46 | 'type': 'number', 47 | 'unit': 'percent', 48 | 'readOnly': True })) 49 | 50 | sht_thing.add_property( 51 | Property( 52 | sht_thing, 53 | 'temperature', 54 | sht_celsius, 55 | metadata={ 56 | '@type': 'TemperatureProperty', 57 | 'title': 'Celsius', 58 | 'type': 'number', 59 | 'unit': '°C', 60 | 'readOnly': True })) 61 | 62 | server = WebThingServer( 63 | MultipleThings([ds18_thing, sht_thing], 'Multi-Sensor'), 64 | port=8890) 65 | 66 | def update(): 67 | t = ds18.temperature() 68 | ds18_celsius.notify_of_external_update(t.C) 69 | 70 | h, t = sht.all() 71 | sht_celsius.notify_of_external_update(t.C) 72 | sht_rh.notify_of_external_update(h.RH) 73 | 74 | timer = tornado.ioloop.PeriodicCallback(update, 3000) 75 | timer.start() 76 | 77 | try: 78 | logging.info('starting the server') 79 | server.start() 80 | except KeyboardInterrupt: 81 | logging.debug('stopping update task') 82 | timer.stop() 83 | logging.info('stopping the server') 84 | server.stop() 85 | logging.info('done') 86 | 87 | 88 | if __name__ == '__main__': 89 | logging.basicConfig( 90 | level=10, 91 | format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s" 92 | ) 93 | run_server() 94 | -------------------------------------------------------------------------------- /examples/webthings/sht20-sensor.py: -------------------------------------------------------------------------------- 1 | from webthing import Thing, Property, Value, SingleThing, WebThingServer 2 | import logging 3 | import tornado.ioloop 4 | from sensor import SHT20 5 | 6 | 7 | def run_server(): 8 | sht = SHT20(1, 0x40) 9 | h, t = sht.all() 10 | celsius = Value(t.C) 11 | humidity = Value(h.RH) 12 | 13 | thing = Thing( 14 | 'urn:dev:ops:humidity-temperature-sensor', 15 | 'SHT20', 16 | ['MultiLevelSensor']) 17 | 18 | thing.add_property( 19 | Property( 20 | thing, 21 | 'humidity', 22 | humidity, 23 | metadata={ 24 | '@type': 'LevelProperty', 25 | 'title': 'Humidity', 26 | 'type': 'number', 27 | 'unit': 'percent', 28 | 'readOnly': True })) 29 | 30 | thing.add_property( 31 | Property( 32 | thing, 33 | 'temperature', 34 | celsius, 35 | metadata={ 36 | '@type': 'LevelProperty', 37 | 'title': 'Temperature', 38 | 'type': 'number', 39 | 'unit': '°C', 40 | 'readOnly': True })) 41 | 42 | server = WebThingServer(SingleThing(thing), port=8889) 43 | 44 | def update(): 45 | h, t = sht.all() 46 | celsius.notify_of_external_update(t.C) 47 | humidity.notify_of_external_update(h.RH) 48 | 49 | timer = tornado.ioloop.PeriodicCallback(update, 3000) 50 | timer.start() 51 | 52 | try: 53 | logging.info('starting the server') 54 | server.start() 55 | except KeyboardInterrupt: 56 | logging.debug('stopping update task') 57 | timer.stop() 58 | logging.info('stopping the server') 59 | server.stop() 60 | logging.info('done') 61 | 62 | 63 | if __name__ == '__main__': 64 | logging.basicConfig( 65 | level=10, 66 | format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s" 67 | ) 68 | run_server() 69 | -------------------------------------------------------------------------------- /sensor/BMP180.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Nick Lee 2 | # Copyright 2014 IIJ Innovation Institute Inc. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # 12 | # THIS SOFTWARE IS PROVIDED BY IIJ INNOVATION INSTITUTE INC. ``AS IS'' AND 13 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | # ARE DISCLAIMED. IN NO EVENT SHALL IIJ INNOVATION INSTITUTE INC. OR 16 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 17 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 18 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 19 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 21 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | # Copyright 2014 Keiichi Shima. All rights reserved. 25 | # 26 | # Redistribution and use in source and binary forms, with or without 27 | # modification, are permitted provided that the following conditions are 28 | # met: 29 | # * Redistributions of source code must retain the above copyright 30 | # notice, this list of conditions and the following disclaimer. 31 | # * Redistributions in binary form must reproduce the above 32 | # copyright notice, this list of conditions and the following 33 | # disclaimer in the documentation and/or other materials provided 34 | # with the distribution. 35 | # 36 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 37 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 38 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 39 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR 40 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 41 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 42 | # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 43 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 44 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 45 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 46 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 47 | 48 | # Include the sensor directory, so this file may be run as a test script. 49 | if __name__ == "__main__" and __package__ is None: 50 | import os, sys 51 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 52 | 53 | import struct 54 | import time 55 | import smbus 56 | import sensor 57 | from sensor.util import Pressure, Temperature 58 | 59 | # Registers 60 | _REG_AC1 = 0xAA 61 | _REG_AC2 = 0xAC 62 | _REG_AC3 = 0xAE 63 | _REG_AC4 = 0xB0 64 | _REG_AC5 = 0xB2 65 | _REG_AC6 = 0xB4 66 | _REG_B1 = 0xB6 67 | _REG_B2 = 0xB8 68 | _REG_MB = 0xBA 69 | _REG_MC = 0xBC 70 | _REG_MD = 0xBE 71 | _REG_CALIB_OFFSET = _REG_AC1 72 | _REG_CONTROL_MEASUREMENT = 0xF4 73 | _REG_DATA = 0xF6 74 | 75 | # Commands 76 | _CMD_START_CONVERSION = 0b00100000 77 | _CMD_TEMPERATURE = 0b00001110 78 | _CMD_PRESSURE = 0b00010100 79 | 80 | # Oversampling mode 81 | OS_MODE_SINGLE = 0b00 82 | OS_MODE_2 = 0b01 83 | OS_MODE_4 = 0b10 84 | OS_MODE_8 = 0b11 85 | 86 | # Conversion time (in second) 87 | _WAIT_TEMPERATURE = 0.0045 88 | _WAIT_PRESSURE = [0.0045, 0.0075, 0.0135, 0.0255] 89 | 90 | class BMP180(sensor.SensorBase): 91 | def __init__(self, bus, addr, os_mode = OS_MODE_SINGLE): 92 | assert(addr > 0b000111 and addr < 0b1111000) 93 | 94 | super(BMP180, self).__init__( 95 | update_callback = self._update_sensor_data) 96 | 97 | self._bus = smbus.SMBus(bus) 98 | self._addr = addr 99 | 100 | self._ac0 = None 101 | self._ac1 = None 102 | self._ac2 = None 103 | self._ac3 = None 104 | self._ac4 = None 105 | self._ac5 = None 106 | self._ac6 = None 107 | self._b1 = None 108 | self._b2 = None 109 | self._mb = None 110 | self._mc = None 111 | self._md = None 112 | self._os_mode = os_mode 113 | self._pressure = None 114 | self._temperature = None 115 | 116 | self._read_calibration_data() 117 | 118 | def pressure(self): 119 | '''Returns a pressure value. Returns None if no valid value is set 120 | yet. 121 | ''' 122 | self._update() 123 | return Pressure(hPa=self._pressure) 124 | 125 | def temperature(self): 126 | '''Returns a temperature value. Returns None if no valid value is 127 | set yet. 128 | ''' 129 | self._update() 130 | return Temperature(C=self._temperature) 131 | 132 | def all(self): 133 | '''Returns pressure and temperature values as a tuple. This call can 134 | save 1 transaction than getting a pressure and temperature 135 | values separetely. Returns (None, None) if no valid values 136 | are set yet. 137 | ''' 138 | self._update() 139 | return (Pressure(hPa=self._pressure), Temperature(C=self._temperature)) 140 | 141 | @property 142 | def os_mode(self): 143 | '''Gets/Sets oversampling mode. 144 | OS_MODE_SINGLE: Single mode. 145 | OS_MODE_2: 2 times. 146 | OS_MODE_4: 4 times. 147 | OS_MODE_8: 8 times. 148 | ''' 149 | return (self._os_mode) 150 | 151 | @os_mode.setter 152 | def os_mode(self, os_mode): 153 | assert(os_mode == OS_MODE_SINGLE 154 | or os_mode == OS_MODE_2 155 | or os_mode == OS_MODE_4 156 | or os_mode == OS_MODE_8) 157 | self._os_mode = os_mode 158 | 159 | def _read_calibration_data(self): 160 | calib = self._bus.read_i2c_block_data(self._addr, 161 | _REG_CALIB_OFFSET, 22) 162 | (self._ac1, self._ac2, self._ac3, self._ac4, 163 | self._ac5, self._ac6, self._b1, self._b2, 164 | self._mb, self._mc, self._md) = struct.unpack( 165 | '>hhhHHHhhhhh', bytes(calib)) 166 | 167 | @sensor.i2c_lock 168 | def _update_sensor_data(self): 169 | cmd = _CMD_START_CONVERSION | _CMD_TEMPERATURE 170 | self._bus.write_byte_data(self._addr, 171 | _REG_CONTROL_MEASUREMENT, cmd) 172 | time.sleep(_WAIT_TEMPERATURE) 173 | vals = self._bus.read_i2c_block_data(self._addr, 174 | _REG_DATA, 2) 175 | ut = vals[0] << 8 | vals[1] 176 | 177 | cmd = _CMD_START_CONVERSION | self._os_mode << 6 | _CMD_PRESSURE 178 | self._bus.write_byte_data(self._addr, 179 | _REG_CONTROL_MEASUREMENT, cmd) 180 | time.sleep(_WAIT_PRESSURE[self._os_mode]) 181 | vals = self._bus.read_i2c_block_data(self._addr, 182 | _REG_DATA, 3) 183 | up = (vals[0] << 16 | vals[1] << 8 | vals[0]) >> (8 - self._os_mode) 184 | 185 | x1 = ((ut - self._ac6) * self._ac5) >> 15 186 | x2 = (self._mc << 11) // (x1 + self._md) 187 | b5 = x1 + x2 188 | self._temperature = ((b5 + 8) // 2**4) / 10.0 189 | 190 | b6 = b5 - 4000 191 | x1 = self._b2 * ((b6 * b6) >> 12) 192 | x2 = self._ac2 * b6 193 | x3 = (x1 + x2) >> 11 194 | b3 = (((self._ac1 *4 + x3) << self._os_mode) + 2) >> 2 195 | x1 = (self._ac3 * b6) >> 13 196 | x2 = (self._b1 * (b6 * b6) >> 12) >> 16 197 | x3 = ((x1 + x2) + 2) >> 2 198 | b4 = (self._ac4 * (x3 + 32768)) >> 15 199 | b7 = (up - b3) * (50000 >> self._os_mode) 200 | if (b7 < 0x80000000): 201 | p = (b7 * 2) // b4 202 | else: 203 | p = (b7 // b4) * 2 204 | x1 = p**2 >> 16 205 | x1 = (x1 * 3038) >> 16 206 | x2 = (-7357 * p) >> 16 207 | self._pressure = (p + ((x1 + x2 + 3791) >> 4)) / 100.0 208 | -------------------------------------------------------------------------------- /sensor/DS18B20.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015 Nick Lee 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, including without limitation 8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | # and/or sell copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Include the sensor directory, so this file may be run as a test script. 24 | if __name__ == "__main__" and __package__ is None: 25 | import os, sys 26 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 27 | 28 | import subprocess 29 | import sensor 30 | from sensor.util import Temperature 31 | 32 | class DS18B20(sensor.SensorBase): 33 | def __init__(self, addr): 34 | super(DS18B20, self).__init__(self._update_sensor_data) 35 | 36 | self._device = '/sys/bus/w1/devices/%s/w1_slave' % addr 37 | self._temperature = None 38 | 39 | def temperature(self): 40 | self._update() 41 | return Temperature(C=self._temperature) if self._temperature is not None else None 42 | 43 | @sensor.w1_lock 44 | def _update_sensor_data(self): 45 | # Try at most 3 times 46 | for i in range(0,3): 47 | # Split output into separate lines. 48 | lines = subprocess.check_output(['cat', self._device]).decode().split('\n') 49 | 50 | # If the first line does not end with 'YES', try again. 51 | if lines[0][-3:] != 'YES': 52 | time.sleep(0.2) 53 | continue 54 | 55 | # If the second line does not have a 't=', try again. 56 | pos = lines[1].find('t=') 57 | if pos < 0: 58 | time.sleep(0.2) 59 | continue 60 | 61 | # Extract the temperature. 62 | self._temperature = float(lines[1][pos+2:]) / 1000.0 63 | return 64 | 65 | # Failed reading 66 | self._temperature = None 67 | -------------------------------------------------------------------------------- /sensor/HTU21D.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Nick Lee 2 | # Copyright 2014 IIJ Innovation Institute Inc. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # 12 | # THIS SOFTWARE IS PROVIDED BY IIJ INNOVATION INSTITUTE INC. ``AS IS'' AND 13 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | # ARE DISCLAIMED. IN NO EVENT SHALL IIJ INNOVATION INSTITUTE INC. OR 16 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 17 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 18 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 19 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 21 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | # Copyright 2014 Keiichi Shima. All rights reserved. 25 | # 26 | # Redistribution and use in source and binary forms, with or without 27 | # modification, are permitted provided that the following conditions are 28 | # met: 29 | # * Redistributions of source code must retain the above copyright 30 | # notice, this list of conditions and the following disclaimer. 31 | # * Redistributions in binary form must reproduce the above 32 | # copyright notice, this list of conditions and the following 33 | # disclaimer in the documentation and/or other materials provided 34 | # with the distribution. 35 | # 36 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 37 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 38 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 39 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR 40 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 41 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 42 | # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 43 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 44 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 45 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 46 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 47 | 48 | # Include the sensor directory, so this file may be run as a test script. 49 | if __name__ == "__main__" and __package__ is None: 50 | import os, sys 51 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 52 | 53 | import fcntl 54 | import io 55 | import struct 56 | import time 57 | import sensor 58 | from sensor.util import Humidity, Temperature 59 | 60 | # fcntl 61 | _I2C_SLAVE = 0x0703 62 | 63 | # Configuration parameters 64 | RESOLUTION_12BITS = 0b00000000 65 | RESOLUTION_8BITS = 0b00000001 66 | RESOLUTION_10BITS = 0b10000000 67 | RESOLUTION_11BITS = 0b10000001 68 | # _END_OF_BATTERY = 0b01000000 69 | # _ENABLE_ONCHIP_HEATER = 0b00000100 70 | _DISABLE_ONCHIP_HEATER = 0b00000000 71 | _ENABLE_OTP_RELOAD = 0b00000000 72 | _DISABLE_OTP_RELOAD = 0b00000010 73 | _RESERVED_BITMASK = 0b00111000 74 | 75 | # Commands 76 | _CMD_TEMPERATURE = b'\xF3' 77 | _CMD_HUMIDITY = b'\xF5' 78 | _CMD_WRITE_CONFIG = b'\xE6' 79 | _CMD_READ_CONFIG = b'\xE7' 80 | _CMD_SOFT_RESET = b'\xFE' 81 | 82 | # Data bits specification 83 | _STATUS_BITMASK = 0b00000011 84 | _STATUS_TEMPERATURE = 0b00000000 85 | _STATUS_HUMIDITY = 0b00000010 86 | _STATUS_LSBMASK = 0b11111100 87 | 88 | class HTU21D(sensor.SensorBase): 89 | SOFT_RESET_DELAY = 0.02 90 | TEMPERATURE_DELAY = 0.05 91 | HUMIDITY_DELAY = 0.02 92 | 93 | def __init__(self, bus, addr, 94 | resolution = RESOLUTION_12BITS, 95 | use_temperature = True): 96 | 97 | assert(addr > 0b0000111 and addr < 0b1111000) 98 | assert(resolution == RESOLUTION_12BITS 99 | or resolution == RESOLUTION_8BITS 100 | or resolution == RESOLUTION_10BITS 101 | or resolution == RESOLUTION_11BITS) 102 | assert(use_temperature == True 103 | or use_temperature == False) 104 | 105 | super(HTU21D, self).__init__(self._update_sensor_data) 106 | 107 | self._ior = io.open('/dev/i2c-' + str(bus), 'rb', buffering=0) 108 | self._iow = io.open('/dev/i2c-' + str(bus), 'wb', buffering=0) 109 | fcntl.ioctl(self._ior, _I2C_SLAVE, addr) 110 | fcntl.ioctl(self._iow, _I2C_SLAVE, addr) 111 | 112 | self._resolution = resolution 113 | self._onchip_heater = _DISABLE_ONCHIP_HEATER 114 | self._otp_reload = _DISABLE_OTP_RELOAD 115 | 116 | self._humidity = None 117 | self._temperature = None 118 | self._use_temperature = use_temperature 119 | 120 | self._reset() 121 | self._reconfigure() 122 | 123 | def humidity(self): 124 | '''Returns a relative humidity value. Returns None if no valid value 125 | is set yet. 126 | ''' 127 | self._update() 128 | return Humidity(RH=self._humidity) 129 | 130 | def temperature(self): 131 | '''Returns a temperature value. Returns None if no valid value is set 132 | yet. 133 | ''' 134 | if self._use_temperature is True: 135 | self._update() 136 | return Temperature(C=self._temperature) 137 | 138 | def all(self): 139 | self._update() 140 | 141 | h = Humidity(RH=self._humidity) 142 | t = Temperature(C=self._temperature) if self._temperature is not None else None 143 | return (h, t) 144 | 145 | @property 146 | def use_temperature(self): 147 | '''Returns whether to measure temperature or not. 148 | ''' 149 | return (self._use_temperature) 150 | 151 | @use_temperature.setter 152 | def use_temperature(self, use_temperature): 153 | assert(use_temperature == True 154 | or use_temperature == False) 155 | self._use_temperature = use_temperature 156 | 157 | @property 158 | def resolution(self): 159 | '''Gets/Sets the resolution of temperature value. 160 | RESOLUTION_12BITS: RH 12 bits, Temp 14 bits. 161 | RESOLUTION_8BITS: RH 8 bits, Temp 12 bits. 162 | RESOLUTION_10BITS: RH 10 bits, Temp 13 bits. 163 | RESOLUTION_11BITS: RH 11 bits, Temp 11 bits. 164 | ''' 165 | return (self._resolution) 166 | 167 | @resolution.setter 168 | def resolution(self, resolution): 169 | assert(resolution == RESOLUTION_12BITS 170 | or resolution == RESOLUTION_8BITS 171 | or resolution == RESOLUTION_10BITS 172 | or resolution == RESOLUTION_11BITS) 173 | self._resolution = resolution 174 | self._reconfigure() 175 | 176 | def _reset(self): 177 | self._iow.write(_CMD_SOFT_RESET) 178 | time.sleep(self.SOFT_RESET_DELAY) 179 | 180 | def _reconfigure(self): 181 | self._iow.write(_CMD_READ_CONFIG) 182 | configs = self._ior.read(1) 183 | (config,) = struct.unpack('B', configs) 184 | config = ((config & _RESERVED_BITMASK) 185 | | self._resolution 186 | | self._onchip_heater 187 | | self._otp_reload) 188 | self._iow.write(_CMD_WRITE_CONFIG + struct.pack('B', config)) 189 | 190 | @sensor.i2c_lock 191 | def _update_sensor_data(self): 192 | if self._use_temperature is True: 193 | self._iow.write(_CMD_TEMPERATURE) 194 | time.sleep(self.TEMPERATURE_DELAY) 195 | vals = self._ior.read(3) 196 | (temphigh, templow, crc) = struct.unpack('BBB', vals) 197 | temp = (temphigh << 8) | (templow & _STATUS_LSBMASK) 198 | self._temperature = -46.85 + (175.72 * temp) / 2**16 199 | 200 | self._iow.write(_CMD_HUMIDITY) 201 | time.sleep(self.HUMIDITY_DELAY) 202 | vals = self._ior.read(3) 203 | (humidhigh, humidlow, crc) = struct.unpack('BBB', vals) 204 | humid = (humidhigh << 8) | (humidlow & _STATUS_LSBMASK) 205 | self._humidity = -6 + (125.0 * humid) / 2**16 206 | -------------------------------------------------------------------------------- /sensor/MCP3004.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015 Nick Lee 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, including without limitation 8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | # and/or sell copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import spidev 24 | import sensor 25 | 26 | class MCP3004(object): 27 | def __init__(self, bus, addr, vref): 28 | super(MCP3004, self).__init__() 29 | 30 | self._vref = vref 31 | self._spi = spidev.SpiDev() 32 | self._spi.open(bus, addr) 33 | 34 | def read(self, channel): 35 | r = self._spi.xfer2([1, (8+channel) << 4, 0]) 36 | out = ((r[1]&3) << 8) + r[2] 37 | return out 38 | 39 | def voltage(self, channel): 40 | return self._vref * self.read(channel) / 1024.0 41 | -------------------------------------------------------------------------------- /sensor/SHT20.py: -------------------------------------------------------------------------------- 1 | from sensor.HTU21D import HTU21D 2 | 3 | class SHT20(HTU21D): 4 | TEMPERATURE_DELAY = 0.1 5 | HUMIDITY_DELAY = 0.04 6 | -------------------------------------------------------------------------------- /sensor/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Nick Lee 2 | # Copyright 2014 IIJ Innovation Institute Inc. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # 12 | # THIS SOFTWARE IS PROVIDED BY IIJ INNOVATION INSTITUTE INC. ``AS IS'' AND 13 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | # ARE DISCLAIMED. IN NO EVENT SHALL IIJ INNOVATION INSTITUTE INC. OR 16 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 17 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 18 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 19 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 21 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | 25 | # Copyright 2014 Keiichi Shima. All rights reserved. 26 | # 27 | # Redistribution and use in source and binary forms, with or without 28 | # modification, are permitted provided that the following conditions are 29 | # met: 30 | # * Redistributions of source code must retain the above copyright 31 | # notice, this list of conditions and the following disclaimer. 32 | # * Redistributions in binary form must reproduce the above 33 | # copyright notice, this list of conditions and the following 34 | # disclaimer in the documentation and/or other materials provided 35 | # with the distribution. 36 | # 37 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 38 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 39 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 40 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR 41 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 42 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 43 | # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 44 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 45 | # IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 46 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 47 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 48 | 49 | import time 50 | import threading 51 | 52 | # Locks for buses: subclasses of SensorBase should apply the appropriate 53 | # decorator(s) to ensure only one device is accessing a particular bus 54 | # at any given moment. 55 | 56 | _w1_lock = threading.Lock() 57 | 58 | def w1_lock(func): 59 | def locked(*args, **kwargs): 60 | with _w1_lock: 61 | func(*args, **kwargs) 62 | return locked 63 | 64 | _i2c_lock = threading.Lock() 65 | 66 | def i2c_lock(func): 67 | def locked(*args, **kwargs): 68 | with _i2c_lock: 69 | func(*args, **kwargs) 70 | return locked 71 | 72 | _spi_lock = threading.Lock() 73 | 74 | def spi_lock(func): 75 | def locked(*args, **kwargs): 76 | with _spi_lock: 77 | func(*args, **kwargs) 78 | return locked 79 | 80 | class SensorBase(object): 81 | def __init__(self, update_callback): 82 | assert (update_callback is not None) 83 | 84 | self._cache_lifetime = 0 85 | self._last_updated = None 86 | self._update_callback = update_callback 87 | 88 | def _update(self, **kwargs): 89 | now = time.time() 90 | 91 | # If caching is disabled, just update the data. 92 | if self._cache_lifetime > 0: 93 | # Check if the cached value is still valid or not. 94 | if (self._last_updated is not None 95 | and self._last_updated + self._cache_lifetime > now): 96 | # The value is still valid. 97 | return 98 | 99 | # Get the latest sensor values. 100 | try: 101 | self._update_callback(**kwargs) 102 | self._last_updated = now 103 | except: 104 | raise 105 | 106 | return 107 | 108 | @property 109 | def cache_lifetime(self): 110 | '''Gets/Sets the cache time (in seconds). 111 | ''' 112 | return (self._cache_lifetime) 113 | 114 | @cache_lifetime.setter 115 | def cache_lifetime(self, cache_lifetime): 116 | assert(cache_lifetime >= 0) 117 | 118 | self._cache_lifetime = cache_lifetime 119 | 120 | 121 | __all__ = ['DS18B20', 'SHT20', 'HTU21D', 'BMP180', 'MCP3004'] 122 | 123 | from .DS18B20 import DS18B20 124 | from .SHT20 import SHT20 125 | from .HTU21D import HTU21D 126 | from .BMP180 import BMP180 127 | from .MCP3004 import MCP3004 128 | -------------------------------------------------------------------------------- /sensor/util.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015 Nick Lee 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the "Software"), 7 | # to deal in the Software without restriction, including without limitation 8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | # and/or sell copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from collections import namedtuple 24 | 25 | class Temperature(namedtuple('Temperature', ['C', 'F', 'K'])): 26 | def __new__(cls, **kwargs): 27 | if len(kwargs) != 1: 28 | raise RuntimeError('Specify temperature in one and only one unit: C, F, K') 29 | 30 | if 'C' in kwargs: 31 | kwargs['F'] = kwargs['C'] * 1.8 + 32 32 | kwargs['K'] = kwargs['C'] + 273.15 33 | elif 'F' in kwargs: 34 | kwargs['C'] = (kwargs['F'] - 32) / 1.8 35 | kwargs['K'] = kwargs['C'] + 273.15 36 | elif 'K' in kwargs: 37 | kwargs['C'] = kwargs['K'] - 273.15 38 | kwargs['F'] = kwargs['C'] * 1.8 + 32 39 | else: 40 | raise RuntimeError('Specify temperature in either: C, F, K') 41 | 42 | return super(Temperature, cls).__new__(cls, **kwargs) 43 | 44 | class Humidity(namedtuple('Humidity', ['RH'])): 45 | pass 46 | 47 | class Altitude(namedtuple('Altitude', ['m', 'ft'])): 48 | def __new__(cls, **kwargs): 49 | if len(kwargs) != 1: 50 | raise RuntimeError('Specify altitude in one and only one unit: m, ft') 51 | 52 | if 'm' in kwargs: 53 | kwargs['ft'] = kwargs['m'] * 3.28083998 54 | elif 'ft' in kwargs: 55 | kwargs['m'] = kwargs['ft'] * 0.3048 56 | else: 57 | raise RuntimeError('Specify altitude in either: m, ft') 58 | 59 | return super(Altitude, cls).__new__(cls, **kwargs) 60 | 61 | class Pressure(namedtuple('Pressure', ['hPa'])): 62 | def altitude(self, msl): 63 | msl = msl.hPa if type(msl) is Pressure else msl 64 | m = 44330 * (1 - (self.hPa / msl)**(1 / 5.255)) 65 | return Altitude(m=m) 66 | 67 | def msl_pressure(self, altitude): 68 | m = altitude.m if type(altitude) is Altitude else altitude 69 | hPa = self.hPa / (1.0 - m/44330.0)**5.255 70 | return Pressure(hPa=hPa) 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | setup( 7 | name='sensor', 8 | packages=['sensor'], 9 | 10 | # List run-time dependencies here. These will be installed by pip when 11 | # your project is installed. For an analysis of "install_requires" vs pip's 12 | # requirements files see: 13 | # https://packaging.python.org/en/latest/requirements.html 14 | install_requires=[], 15 | 16 | # Versions should comply with PEP440. For a discussion on single-sourcing 17 | # the version across setup.py and the project code, see 18 | # https://packaging.python.org/en/latest/single_source_version.html 19 | version='6', 20 | 21 | description='Raspberry Pi Sensors', 22 | 23 | # The project's main homepage. 24 | url='https://github.com/nickoala/sensor', 25 | 26 | # Author details 27 | author='Nick Lee', 28 | author_email='lee1nick@yahoo.ca', 29 | 30 | # Choose your license 31 | license='MIT', 32 | 33 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 34 | classifiers=[ 35 | # How mature is this project? Common values are 36 | # 3 - Alpha 37 | # 4 - Beta 38 | # 5 - Production/Stable 39 | 'Development Status :: 4 - Beta', 40 | 41 | # Indicate who your project is intended for 42 | 'Intended Audience :: Developers', 43 | 'Intended Audience :: Education', 44 | 'Topic :: Home Automation', 45 | 'Topic :: Software Development :: Libraries :: Python Modules', 46 | 47 | # Pick your license as you wish (should match "license" above) 48 | 'License :: OSI Approved :: MIT License', 49 | 50 | # Specify the Python versions you support here. In particular, ensure 51 | # that you indicate whether you support Python 2, Python 3 or both. 52 | 'Programming Language :: Python :: 3.5', 53 | ], 54 | 55 | # What does your project relate to? 56 | keywords='raspberry pi raspi rpi sensor', 57 | ) 58 | --------------------------------------------------------------------------------