pfSense stat monitor

@cweakland How do you find the baseoid of each sensor?

There’s a website you can use to lookup most OIDs:

http://www.oid-info.com/basic-search.htm

Let say you want to poll for interface em1, start with a snmpwalk:

$ snmpwalk -v2c -cpublic 10.1.1.1 1.3.6.1.2.1.31.1.1.1
iso.3.6.1.2.1.31.1.1.1.1.1 = STRING: “em0”
iso.3.6.1.2.1.31.1.1.1.1.2 = STRING: "em1"
iso.3.6.1.2.1.31.1.1.1.1.3 = STRING: “enc0”
iso.3.6.1.2.1.31.1.1.1.1.4 = STRING: “lo0”
iso.3.6.1.2.1.31.1.1.1.1.5 = STRING: “pflog0”
iso.3.6.1.2.1.31.1.1.1.1.6 = STRING: “pfsync0”
iso.3.6.1.2.1.31.1.1.1.1.7 = STRING: “em0.10”
iso.3.6.1.2.1.31.1.1.1.1.8 = STRING: “em0.20”
iso.3.6.1.2.1.31.1.1.1.1.9 = STRING: “em0.30”
iso.3.6.1.2.1.31.1.1.1.1.10 = STRING: “em0.40”

Next walk this branch:

$ snmpwalk -v2c -cpublic 10.1.1.1 1.3.6.1.2.1.31.1.1.1.6
iso.3.6.1.2.1.31.1.1.1.6.1 = Counter64: 2268394748162
iso.3.6.1.2.1.31.1.1.1.6.2 = Counter64: 2680156151093
iso.3.6.1.2.1.31.1.1.1.6.7 = Counter64: 3454055199
iso.3.6.1.2.1.31.1.1.1.6.8 = Counter64: 696671193
iso.3.6.1.2.1.31.1.1.1.6.9 = Counter64: 1631522976516
iso.3.6.1.2.1.31.1.1.1.6.10 = Counter64: 3773209828

Notice how the counters in the second walk align with the network interfaces from the first walk. In this case for em1 I want to use 1.3.6.1.2.1.31.1.1.1.6.2 . Let me know if you need more help.

Are those command in pfsense shell? Command wasnt recognized.

I would do this from a linux device, I believe you need to have the “snmp” package installed. You will need to setup snmp polling on pfsense and allow the linux device to poll it.

There are SNMP walk applications for windows as well.

Hrm. It looks like you interrupted it before it received any data. Do you get your stats_file (“pfSense_stats.json” in my example) to print? I’m not sure what the default timeout is for GET call, but it looks like it attempted to make the connection.

Okay seems to be working. Thanks. Im pretty sure I have the right data.

Hey Guys, I followed every thing, updated the broken parts of the scripts, and now I’m stuck, because we can’t execute python scripts over command line as it seems.

Basically I get :

Command failed: python /config/pfsense_monitoring.py […]

Same thing if I put “python3”.

Is there a workaround with the python script integration ?

Hello everyone and congratulations for the integration.

I state that I am a noob
I have some problem from point 4 and the import json file, i put my configuration:

PFSENSE
i installed the pkg on the pfsense and modify the config.ini
First question:
where the client is to be installed “pip3 install pfsense-fauxapi”? on the pfsense or hass.io

i have this file into /config/custom_components/pfSense
init.py
version.py
pffa_get_system_stats.py
PfsenseFauxapi.py

This detail for file:

init.py

from .__version__ import __version__
from .PfsenseFauxapi import PfsenseFauxapi
from .PfsenseFauxapi import PfsenseFauxapiException

version.py

__version__ = '20190317.1'

pffa_get_system_stats.py

import os, sys, json
sys.path.append(os.path.abspath(os.path.join(os.path.curdir, '../config/custom_components/pfSense')))     # hack to make this work in-place
from PfsenseFauxapi import PfsenseFauxapi


# check args exist
if(len(sys.argv) < 4):
    print()
    print('usage: ' + sys.argv[0] + ' 192.168.10.254 apikeypfsense 12345678900987654321')
    print()
    print('pipe JSON output through jq for easy pretty print output:-')
    print(' $ ' + sys.argv[0] + ' 192.168.10.254 apikeypfsense 12345678900987654321 | jq .')
    print()
    sys.exit(1)

# config
fauxapi_host=sys.argv[1]
fauxapi_apikey=sys.argv[2]
fauxapi_apisecret=sys.argv[3]

PfsenseFauxapi = PfsenseFauxapi(fauxapi_host, fauxapi_apikey, fauxapi_apisecret, debug=False)


# system_stats
# =============================================================================
print(json.dumps(
    PfsenseFauxapi.system_stats())
)
open('pfSense_stats.json', 'w').close()  # clear data
with open('pfSense_stats.json', 'a') as stat_file:
    print(json.dumps(
    PfsenseFauxapi.system_stats()), file=stat_file)

PfsenseFauxapi.py

#
# Copyright 2017 Nicholas de Jong  <contact[at]nicholasdejong.com>
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# 

import os
import json
import base64
import urllib
import requests
import datetime
import hashlib
from __version__ import __version__


class PfsenseFauxapiException(Exception):
    pass


class PfsenseFauxapi:

    host = None
    proto = None
    debug = None
    version = None
    apikey = None
    apisecret = None
    use_verified_https = None

    def __init__(self, host, apikey, apisecret, use_verified_https=False, debug=False):
        self.proto = 'https'
        self.base_url = 'fauxapi/v1'
        self.version = __version__
        self.host = host
        self.apikey = apikey
        self.apisecret = apisecret
        self.use_verified_https = use_verified_https
        self.debug = debug
        if self.use_verified_https is False:
            requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

    def config_get(self, section=None):
        config = self._api_request('GET', 'config_get')
        if section is None:
            return config['data']['config']
        elif section in config['data']['config']:
            return config['data']['config'][section]
        raise PfsenseFauxapiException('Unable to complete config_get request, section is unknown', section)

    def config_set(self, config, section=None):
        if section is None:
            config_new = config
        else:
            config_new = self.config_get(section=None)
            config_new[section] = config
        return self._api_request('POST', 'config_set', data=json.dumps(config_new))

    def config_patch(self, config):
        return self._api_request('POST', 'config_patch', data=json.dumps(config))

    def config_reload(self):
        return self._api_request('GET', 'config_reload')

    def config_backup(self):
        return self._api_request('GET', 'config_backup')

    def config_backup_list(self):
        return self._api_request('GET', 'config_backup_list')

    def config_restore(self, config_file):
        return self._api_request('GET', 'config_restore', params={'config_file': config_file})

    def send_event(self, command):
        return self._api_request('POST', 'send_event', data=json.dumps([command]))

    def system_reboot(self):
        return self._api_request('GET', 'system_reboot')

    def system_stats(self):
        return self._api_request('GET', 'system_stats')

    def interface_stats(self, interface):
        return self._api_request('GET', 'interface_stats', params={'interface': interface})

    def gateway_status(self):
        return self._api_request('GET', 'gateway_status')

    def rule_get(self, rule_number=None):
        return self._api_request('GET', 'rule_get', params={'rule_number': rule_number})

    def alias_update_urltables(self, table=None):
        if table is not None:
            return self._api_request('GET', 'alias_update_urltables', params={'table': table})
        return self._api_request('GET', 'alias_update_urltables')

    def function_call(self, data):
        return self._api_request('POST', 'function_call', data=json.dumps(data))

    def _api_request(self, method, action, params=None, data=None):

        if params is None:
            params = {}

        if self.debug:
            params['__debug'] = 'true'

        url = '{proto}://{host}/{base_url}/?action={action}&{params}'.format(
            proto=self.proto, host=self.host, base_url=self.base_url, action=action, params=urllib.parse.urlencode(params))

        if method.upper() == 'GET':
            res = requests.get(
                url,
                headers={'fauxapi-auth': self._generate_auth()},
                verify=self.use_verified_https
            )
        elif method.upper() == 'POST':
            res = requests.post(
                url,
                headers={'fauxapi-auth': self._generate_auth()},
                verify=self.use_verified_https,
                data=data
            )
        else:
            raise PfsenseFauxapiException('Request method not supported!', method)

        if res.status_code == 404:
            raise PfsenseFauxapiException('Unable to find FauxAPI on target host, is it installed?')
        elif res.status_code != 200:
            raise PfsenseFauxapiException('Unable to complete {}() request'.format(action), json.loads(res.text))

        return self._json_parse(res.text)

    def _generate_auth(self):
        # auth = apikey:timestamp:nonce:HASH(apisecret:timestamp:nonce)
        nonce = base64.b64encode(os.urandom(40)).decode('utf-8').replace('=', '').replace('/', '').replace('+', '')[0:8]
        timestamp = datetime.datetime.utcnow().strftime('%Y%m%dZ%H%M%S')
        hash = hashlib.sha256('{}{}{}'.format(self.apisecret, timestamp, nonce).encode('utf-8')).hexdigest()
        return '{}:{}:{}:{}'.format(self.apikey, timestamp, nonce, hash)

    def _json_parse(self, data):
        try:
            return json.loads(data)
        except json.JSONDecodeError:
            pass
        raise PfsenseFauxapiException('Unable to parse response data!', data)

Now i don’t have any JSON file into any configuration folder.

this my configuration yaml

# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:

# Uncomment this if you are using SSL/TLS, running in Docker container, etc.
# http:
#   base_url: example.duckdns.org:8123
  http:
    base_url: !secret base_url
    use_x_forwarded_for: true
    trusted_proxies: 192.168.10.254
#GOOGLE HOME

google_assistant:
  project_id: !secret google_projectid
  service_account: !include robere.json

# Text to speech
tts:
  - platform: google_translate
#HACS
hacs:
  token: !secret token_hacs
#CONFIGURATION FILE
group: !include groups.yaml
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
sensor: !include sensors.yaml
#switch: !include switches.yaml
#light: !include lights.yaml

#THEMES
frontend:
  themes: !include_dir_merge_named themes/

and sensor.yaml

#PFSENSE
   #this will call the python script and store the cpu temp information in a sensor. the script will export 
   #the rest of the data into a file. we cant dump all of the data into 1 sensor since there is a 255 character 
   #limit for the sensor.
 - platform: command_line
   command: "python3 /config/custom_components/pfSense/pffa_get_system_stats.py 192.168.10.254 apikeypfsense 12345678900987654321"
   name: pfSense_CPU_temp
   value_template: '{{ value_json["data"]["stats"]["temp"] }}'
   unit_of_measurement : 'C'
    
 - platform: file
   file_path: /config/pfSense_stats.json
   name: pfSense_uptime
   value_template: '{{ value_json["data"]["stats"]["uptime"] }}' 
 - platform: file
   file_path: /config/pfSense_stats.json
   name: pfSense_mem
   value_template: '{{ value_json["data"]["stats"]["mem"] }}'
   unit_of_measurement : '%'
 - platform: file
   file_path: /config/pfSense_stats.json
   name: pfSense_cpu
   value_template: '{{ ( ( ((value_json["data"]["stats"]["cpu"].split("|")[0] | float) / (value_json["data"]["stats"]["cpu"].split("|")[1] | float)) - 1.0 ) * 100.0 ) | round(1) }}'
   unit_of_measurement : '%'

 - platform: file
   file_path: /config/pfSense_stats.json
   name: pfSense_mbufpercent
   value_template: '{{ value_json["data"]["stats"]["mbufpercent"] }}'
   unit_of_measurement : '%'

sorry for the length of the post but I’m going crazy

Im test the run scrypt and this is results:

Traceback (most recent call last):
File “pffa_get_system_stats.py”, line 3, in
from PfsenseFauxapi import PfsenseFauxapi
File “/config/custom_components/pfSense/PfsenseFauxapi.py”, line 21, in
import requests
ModuleNotFoundError: No module named ‘requests’

This great! Do you also have Temperature OID and, CPU, Memory usage?
thanks!

Lately I found the CPU utilization stays fixed at 2.7%. By looking at the json “cpu” data, though the data is changing overtime, the resulted calculation will always gives value 2.7%. I therefore use the “load_average” 1-minute value instead. So the CPU load (my CPU has 4 cores, so divided the value by 4) in my configuration.yaml is:

  - platform: file
    file_path: pfSense_stats.json
    name: pfSense_cpu
#   below formula used to work, but lately "cpu" always produced fixed value 2.7%, then use "load_average" 1-min value instead
#    value_template: '{{ ( ( ((value_json["data"]["stats"]["cpu"].split("|")[0] | float) / (value_json["data"]["stats"]["cpu"].split("|")[1] | float)) - 1.0 ) * 100.0 ) | round(1) }}'
    value_template: '{{ ((value_json["data"]["stats"]["load_average"][0] | float) * 100.0 / 4.0 ) | round(0) }}'
    unit_of_measurement : '%'

Good find! :+1: I have the same problem… but I never looked into it.

1 Like

I needed a way to restart my ISP’s crappy modem whenever the internet stopped operating.

I’ve put together a small custom component for Home Assistant which based on alexpmorris’s pfsense-status-gateways-json script checks the gateway’s status in pfSense every 3 minutes.

Multiple gateways can be added to the config, so for every gateway it will create a sensor returning True while the gateway monitoring in pfSense says it’s up, and False whenever pfSense says it’s down.

I power the ISP modem through a Sonoff Mini running Tasmota, connected to a Normally Closed relay, and set with parameter PulseTime 120. This results in the modem being powered on normally, but whenever the gateway goes down for more than 5 mins (according to the automation set in HA), the Sonoff turns on, which cuts the power with the relay. PulseTime 120 means that the Sonoff will turn off after 20 seconds, power will be restored to the modem - this is how I get a 20-second power-cycle on the modem. So in the automation it’s just enough to turn on the switch - it will turn itself off after 20 seconds.

1 Like

I just implemented this. It was considerably easier than having to deploy additional components.

Thanks for providing this information!

I can get the python script to work in terminal, but it fails when run from the command line sensor.

I don’t want to hijack the tread so posted here: Python script works in terminal, but fails in command line sensor

Any help appreciated!

After many tries, I got it mostly working
I complied mine into a github repo in case anyone wants to check it out

4 Likes

Ooo. I like those rule switches. I might have to dig into that :nerd_face:

1 Like

Updated a bit and added HACS install support