Skip to content

Commit f7c512a

Browse files
committed
Merge branch 'jmashon/working-branch' of github.com:FarmBot-Labs/sidecar-starter-pack into jmashon/working-branch
2 parents db6b2f5 + 25369f7 commit f7c512a

10 files changed

+281
-74
lines changed
5.47 KB
Binary file not shown.
2.32 KB
Binary file not shown.
11 KB
Binary file not shown.

__pycache__/fbapi.cpython-312.pyc

5.48 KB
Binary file not shown.

__pycache__/fbbro.cpython-312.pyc

2.32 KB
Binary file not shown.

farmbot_broker.py

-52
This file was deleted.

farmbot_util_PORT.py

+115-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
# farmbot_utilities.py
1+
import sys
2+
import os
23

3-
import json
4-
5-
from farmbot_broker import FarmbotBroker
6-
from farmbot_api import FarmbotAPI
4+
from fbbro import FarmbotBroker
5+
from fbapi import FarmbotAPI
76

87
RPC_REQUEST = {
98
"kind": "rpc_request",
@@ -17,8 +16,8 @@ def __init__(self):
1716
self.broker = FarmbotBroker()
1817
self.api = FarmbotAPI()
1918

20-
self.token = None
21-
self.error = None
19+
self.token = self.api.token
20+
self.error = self.api.error
2221

2322
self.echo = True # Choose whether functions print return statement
2423
self.verbose = True # Choose how much detail in return statement
@@ -87,6 +86,36 @@ def read_status(self):
8786
self.broker.publish(status_message)
8887
# return ...
8988

89+
def read_sensor(self, id, mode, label='---'):
90+
read_sensor_message = {
91+
**RPC_REQUEST,
92+
"body": [{
93+
"kind": "read_pin",
94+
"args": {
95+
"pin_mode": mode,
96+
"label": label,
97+
"pin_number": {
98+
"kind": "named_pin",
99+
"args": {
100+
"pin_type": "Peripheral",
101+
"pin_id": id
102+
}
103+
}
104+
}
105+
}]
106+
}
107+
108+
self.broker.publish(read_sensor_message)
109+
# return ...
110+
111+
def env(self, id=None, field=None, new_val=None):
112+
if id is None:
113+
data = self.api.get('farmware_envs', id=None)
114+
print(data)
115+
else:
116+
data = self.api.get('farmware_envs', id)
117+
print(data)
118+
90119
# MESSAGES
91120
# ├── [✅] log()
92121
# ├── [✅] message()
@@ -240,6 +269,85 @@ def control_servo(self, pin, angle):
240269
self.broker.publish(control_servo_message)
241270
# return ...
242271

272+
def control_servo(self, pin, angle):
273+
if angle < 0 or angle > 180:
274+
return print("ERROR: Servo angle constrained to 0-180 degrees.")
275+
else:
276+
control_servo_message = {
277+
**RPC_REQUEST,
278+
"body": {
279+
"kind": "set_servo_angle",
280+
"args": {
281+
"pin_number": pin,
282+
"pin_value": angle # From 0 to 180
283+
}
284+
}
285+
}
286+
287+
self.broker.publish(control_servo_message)
288+
# return ...
289+
290+
def control_peripheral(self, id, value, mode=None):
291+
if mode is None:
292+
peripheral_str = self.get_info('peripherals', id)
293+
mode = peripheral_str['mode']
294+
295+
control_peripheral_message = {
296+
**RPC_REQUEST,
297+
"body": {
298+
"kind": "write_pin",
299+
"args": {
300+
"pin_value": value, # Controls ON/OFF or slider value from 0-255
301+
"pin_mode": mode, # Controls digital (0) or analog (1) mode
302+
"pin_number": {
303+
"kind": "named_pin",
304+
"args": {
305+
"pin_type": "Peripheral",
306+
"pin_id": id
307+
}
308+
}
309+
}
310+
}
311+
}
312+
313+
self.broker.publish(control_peripheral_message)
314+
# return ...
315+
316+
def toggle_peripheral(self, id):
317+
toggle_peripheral_message = {
318+
**RPC_REQUEST,
319+
"body": [{
320+
"kind": "toggle_pin",
321+
"args": {
322+
"pin_number": {
323+
"kind": "named_pin",
324+
"args": {
325+
"pin_type": "Peripheral",
326+
"pin_id": id
327+
}
328+
}
329+
}
330+
}]
331+
}
332+
333+
self.broker.publish(toggle_peripheral_message)
334+
# return ...
335+
336+
def on(self, id):
337+
peripheral_str = self.get_info('peripherals', id)
338+
mode = peripheral_str['mode']
339+
340+
if mode == 1:
341+
self.control_peripheral(id, 255)
342+
elif mode == 0:
343+
self.control_peripheral(id, 1)
344+
345+
# return ...
346+
347+
def off(self, id):
348+
self.control_peripheral(id, 0)
349+
# return ...
350+
243351
def take_photo(self):
244352
take_photo_message = {
245353
**RPC_REQUEST,

farmbot_api.py fbapi.py

+12-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
# farmbot_API.py
2-
31
import sys
2+
import os
43
import json
54
import requests
65

@@ -9,19 +8,17 @@ def __init__(self):
98
self.token = None
109
self.error = None
1110

12-
# API
13-
# ├── token_handling()
14-
# ├── request_handling()
15-
# │
16-
# ├── get_token()
17-
# ├── check_token()
18-
# │
19-
# ├── request()
20-
# │
21-
# ├── get()
22-
# ├── post()
23-
# ├── patch()
24-
# └── delete()
11+
# token_handling() --> errors for token
12+
# request_handling() --> errors for request
13+
14+
# get_token()
15+
# check_token()
16+
17+
# request()
18+
# get() --> get endpoint info
19+
# post() --> overwrite/new endpoint info
20+
# patch() --> edit endpoint info
21+
# delete() --> delete endpoint info
2522

2623
def token_handling(self, response):
2724
# Handle HTTP status codes

fbbro.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from fbapi import FarmbotAPI
2+
from datetime import datetime
3+
import paho.mqtt.client as mqtt
4+
5+
import json
6+
7+
class BrokerConnect():
8+
def __init__(self):
9+
self.api = FarmbotAPI()
10+
11+
self.token = self.api.token
12+
self.client = None
13+
14+
# connect() --> establish connection to message broker
15+
# disconnect() --> disconnect from message broker
16+
17+
# publish()
18+
19+
# on_connect() --> subscribe to channel
20+
# on_message() --> print channel/message
21+
22+
# def on_connect(self, client, *_args):
23+
# # subscribe to all channels
24+
# self.client.subscribe(f"bot/{self.token['token']['unencoded']['bot']}/#")
25+
# print('connected')
26+
27+
# def on_message(self, _client, _userdata, msg):
28+
# print('-' * 100)
29+
# # print channel
30+
# print(f'{msg.topic} ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})\n')
31+
# # print message
32+
# print(json.dumps(json.loads(msg.payload), indent=4))
33+
34+
# def on_connect(client, *_args):
35+
# # subscribe to all channels
36+
# client.subscribe(f"bot/{TOKEN['token']['unencoded']['bot']}/#")
37+
# print('connected')
38+
39+
def status_connect(self, client, *_args):
40+
# Subscribe to specific channel
41+
device_info = self.api.get('device')
42+
device_id = device_info['id']
43+
44+
client.subscribe("bot/device_4652/status")
45+
print('connected via status_connect()')
46+
47+
def on_message(self, _client, _userdata, msg):
48+
print('-' * 100)
49+
# print channel
50+
print("Channel:")
51+
print(f'{msg.topic} ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})\n')
52+
# print message
53+
print("Message:")
54+
print(json.dumps(json.loads(msg.payload), indent=4))
55+
56+
def connect(self):
57+
print(self.api.token)
58+
59+
self.api.check_token()
60+
61+
self.client = mqtt.Client()
62+
self.client.username_pw_set(
63+
username=self.token['token']['unencoded']['bot'],
64+
password=self.token['token']['encoded']
65+
)
66+
67+
self.client.connect(
68+
self.token['token']['unencoded']['mqtt'],
69+
port=1883,
70+
keepalive=60
71+
)
72+
73+
# self.client.on_connect = status_connect
74+
# self.client.on_message = on_message
75+
76+
self.client.loop_start()
77+
78+
def disconnect(self):
79+
if self.client is not None:
80+
self.client.loop_stop()
81+
self.client.disconnect()
82+
83+
def publish(self, message):
84+
if self.client is None:
85+
self.connect()
86+
87+
self.client.publish(
88+
f'bot/{self.token["token"]["unencoded"]["bot"]}/from_clients',
89+
payload=json.dumps(message)
90+
)

test_api.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
from farmbot_util_PORT import Farmbot
4+
5+
class TestFarmbot(unittest.TestCase):
6+
7+
def setUp(self):
8+
self.farmbot = Farmbot()
9+
10+
@patch('farmbot_util_PORT.FarmbotAPI.get_token')
11+
def test_get_token(self, mock_get_token):
12+
mock_get_token.return_value = 'fake_token'
13+
self.farmbot.get_token('[email protected]', 'password123')
14+
self.assertEqual(self.farmbot.token, 'fake_token')
15+
mock_get_token.assert_called_once_with('[email protected]', 'password123', 'https://my.farm.bot')
16+
17+
@patch('farmbot_util_PORT.FarmbotAPI.get_info')
18+
def test_get_info(self, mock_get_info):
19+
mock_get_info.return_value = {'info': 'fake_info'}
20+
result = self.farmbot.get_info()
21+
self.assertEqual(result, {'info': 'fake_info'})
22+
mock_get_info.assert_called_once()
23+
24+
@patch('farmbot_util_PORT.FarmbotAPI.set_info')
25+
def test_set_info(self, mock_set_info):
26+
self.farmbot.set_info('label', 'value')
27+
mock_set_info.assert_called_once_with('label', 'value')
28+
29+
@patch('farmbot_util_PORT.FarmbotAPI.log')
30+
def test_log(self, mock_log):
31+
self.farmbot.log('message', 'info')
32+
mock_log.assert_called_once_with('message', 'info')
33+
34+
@patch('farmbot_util_PORT.FarmbotAPI.safe_z')
35+
def test_safe_z(self, mock_safe_z):
36+
mock_safe_z.return_value = 10
37+
result = self.farmbot.safe_z()
38+
self.assertEqual(result, 10)
39+
mock_safe_z.assert_called_once()
40+
41+
@patch('farmbot_util_PORT.FarmbotAPI.garden_size')
42+
def test_garden_size(self, mock_garden_size):
43+
mock_garden_size.return_value = {'x': 1000, 'y': 2000}
44+
result = self.farmbot.garden_size()
45+
self.assertEqual(result, {'x': 1000, 'y': 2000})
46+
mock_garden_size.assert_called_once()
47+
48+
@patch('farmbot_util_PORT.FarmbotAPI.group')
49+
def test_group(self, mock_group):
50+
sequences = ['seq1', 'seq2']
51+
mock_group.return_value = {'grouped': True}
52+
result = self.farmbot.group(sequences)
53+
self.assertEqual(result, {'grouped': True})
54+
mock_group.assert_called_once_with(sequences)
55+
56+
@patch('farmbot_util_PORT.FarmbotAPI.curve')
57+
def test_curve(self, mock_curve):
58+
mock_curve.return_value = {'curve': True}
59+
result = self.farmbot.curve('seq', 0, 0, 10, 10, 5, 5)
60+
self.assertEqual(result, {'curve': True})
61+
mock_curve.assert_called_once_with('seq', 0, 0, 10, 10, 5, 5)
62+
63+
if __name__ == '__main__':
64+
unittest.main()

0 commit comments

Comments
 (0)